Public Access
maj auto 2
This commit is contained in:
1 parent
d47bd67391
commit
f91b5ae215
11 files changed
+647
-477
No files matched your search
@@ -0,0 +1,33 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from openai import OpenAI
|
||||||
|
from groq import Groq
|
||||||
|
from google import genai
|
||||||
|
|
||||||
|
# Chargement des variables d'environnement en premier
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
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 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"
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def nettoyer_vieilles_discussions():
|
||||||
|
history = load_history()
|
||||||
|
now = time.time()
|
||||||
|
un_an_en_secondes = 365 * 24 * 60 * 60
|
||||||
|
a_supprimer = [cid for cid, data in history.items() if not data.get('pinned', False) and now - data.get('timestamp', now) > un_an_en_secondes]
|
||||||
|
if a_supprimer:
|
||||||
|
for cid in a_supprimer:
|
||||||
|
del history[cid]
|
||||||
|
save_history(history)
|
||||||
|
print(f"🧹 AETHAS CORE : Nettoyage auto terminé. {len(a_supprimer)} discussion(s) supprimée(s).")
|
||||||
@@ -1,49 +1,19 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from fasthtml.common import *
|
from fasthtml.common import *
|
||||||
from starlette.responses import FileResponse
|
from starlette.responses import FileResponse
|
||||||
from openai import OpenAI
|
|
||||||
from groq import Groq
|
|
||||||
from google import genai
|
|
||||||
|
|
||||||
# ==========================================
|
# Import propre depuis le dossier modules
|
||||||
# SECTION 1. INITIALISATION, SÉCURITÉ & HISTORIQUE
|
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()
|
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()
|
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"
|
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://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/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/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="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}"),
|
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; }
|
.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; }
|
.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); }
|
.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; box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: 0.3s; }
|
.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-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); }
|
.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 { 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-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-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); }
|
.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-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; }
|
.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-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: 4px 10px; border-radius: 12px; font-size: 0.8em; display: flex; align-items: center; gap: 6px; }
|
.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-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; }
|
.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 { 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: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; 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-indicator { display: none; }
|
||||||
.htmx-request .htmx-indicator { display: flex; }
|
.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; }
|
||||||
|
|
||||||
.tech-loader-container { display: flex; align-items: center; gap: 15px; color: var(--accent-blue); font-style: italic; background: var(--bg-panel); padding: 15px 20px; border-radius: 15px 15px 15px 0; border: 1px dashed var(--accent-blue); width: fit-content; margin-bottom: 20px; font-size: 0.9em; box-shadow: 0 0 10px rgba(0, 229, 255, 0.1); }
|
|
||||||
.tech-spinner { width: 20px; height: 20px; border: 3px solid rgba(0, 229, 255, 0.2); border-top-color: var(--accent-blue); border-radius: 50%; animation: spin 1s linear infinite; }
|
.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); } }
|
||||||
"""),
|
"""),
|
||||||
@@ -122,332 +89,220 @@ theme_hdrs = (
|
|||||||
document.querySelectorAll('.markdown-raw').forEach(el => {
|
document.querySelectorAll('.markdown-raw').forEach(el => {
|
||||||
if (el.dataset.rendered) return;
|
if (el.dataset.rendered) return;
|
||||||
let container = el.nextElementSibling;
|
let container = el.nextElementSibling;
|
||||||
|
container.innerHTML = (typeof marked !== 'undefined') ? marked.parse(el.textContent) : el.textContent;
|
||||||
if (typeof marked !== 'undefined') {
|
|
||||||
container.innerHTML = marked.parse(el.textContent);
|
|
||||||
} else {
|
|
||||||
container.innerText = el.textContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
el.dataset.rendered = "true";
|
el.dataset.rendered = "true";
|
||||||
el.classList.remove('markdown-raw');
|
el.classList.remove('markdown-raw');
|
||||||
|
|
||||||
container.querySelectorAll('pre').forEach(pre => {
|
container.querySelectorAll('pre').forEach(pre => {
|
||||||
try {
|
try {
|
||||||
let code = pre.querySelector('code');
|
let code = pre.querySelector('code');
|
||||||
if(code) {
|
if (code && !pre.previousElementSibling?.classList.contains('code-header')) {
|
||||||
let rawLang = code.className.replace('language-', '') || 'texte';
|
let rawLang = code.className.replace('language-', '') || 'snippet';
|
||||||
let lang = rawLang.toUpperCase();
|
|
||||||
let header = document.createElement('div');
|
let header = document.createElement('div');
|
||||||
header.className = 'code-header';
|
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);
|
pre.parentNode.insertBefore(header, pre);
|
||||||
if (typeof hljs !== 'undefined') hljs.highlightElement(code);
|
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');
|
let chat = document.getElementById('chat-history');
|
||||||
if (chat) chat.scrollTop = chat.scrollHeight;
|
if (chat) chat.scrollTop = chat.scrollHeight;
|
||||||
} catch(globalErr) {
|
} catch(e) { console.error("Erreur markdown", e); }
|
||||||
console.error("Aethas - Erreur fatale rendu Markdown", globalErr);
|
}
|
||||||
|
|
||||||
|
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) {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
try {
|
const ta = document.getElementById('prompt-input');
|
||||||
let renderedNode = btn.closest('.ai-bubble').querySelector('.markdown-rendered');
|
const dropZone = document.querySelector('.input-container-box');
|
||||||
let rawNode = btn.closest('.ai-bubble').querySelector('.markdown-raw');
|
const fileInput = document.getElementById('file-upload');
|
||||||
let originalContent = (renderedNode && renderedNode.innerHTML.trim() !== "") ? renderedNode : rawNode;
|
|
||||||
|
if (ta) {
|
||||||
let content = originalContent.cloneNode(true);
|
ta.addEventListener('input', () => autoResize(ta));
|
||||||
let wrapper = document.createElement('div');
|
ta.addEventListener('paste', (e) => {
|
||||||
wrapper.style.backgroundColor = '#ffffff';
|
let items = (e.clipboardData || e.originalEvent.clipboardData).items;
|
||||||
wrapper.style.padding = '20px';
|
let dt = new DataTransfer();
|
||||||
wrapper.style.fontFamily = 'Arial, sans-serif';
|
if (fileInput && fileInput.files) {
|
||||||
wrapper.style.color = '#000000';
|
for (let f of fileInput.files) dt.items.add(f);
|
||||||
|
}
|
||||||
content.querySelectorAll('*').forEach(el => {
|
let hasFiles = false;
|
||||||
el.style.color = '#000000';
|
for (let i = 0; i < items.length; i++) {
|
||||||
if (el.tagName === 'PRE' || el.tagName === 'CODE' || el.tagName === 'TABLE' || el.tagName === 'TH' || el.tagName === 'TD') {
|
if (items[i].kind === 'file') {
|
||||||
el.style.backgroundColor = '#f4f4f4';
|
let blob = items[i].getAsFile();
|
||||||
el.style.borderColor = '#aaaaaa';
|
if (blob) { dt.items.add(blob); hasFiles = true; }
|
||||||
el.style.borderWidth = '1px';
|
}
|
||||||
el.style.borderStyle = 'solid';
|
}
|
||||||
el.style.borderCollapse = 'collapse';
|
if (hasFiles) {
|
||||||
|
e.preventDefault();
|
||||||
|
fileInput.files = dt.files;
|
||||||
|
handleFileSelection(fileInput);
|
||||||
} else {
|
} 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) {
|
if (dropZone && fileInput) {
|
||||||
let rendered = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText;
|
['dragenter', 'dragover'].forEach(eName => dropZone.addEventListener(eName, e => { e.preventDefault(); dropZone.style.borderColor = 'var(--accent-blue)'; }));
|
||||||
let raw = btn.closest('.ai-bubble').querySelector('.markdown-raw').innerText;
|
['dragleave', 'drop'].forEach(eName => dropZone.addEventListener(eName, e => { e.preventDefault(); dropZone.style.borderColor = '#444'; }));
|
||||||
let finalContent = rendered ? rendered : raw;
|
dropZone.addEventListener('drop', (e) => {
|
||||||
let blob = new Blob([finalContent], {type: 'text/plain;charset=utf-8'});
|
e.preventDefault();
|
||||||
let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.txt';
|
let dt = new DataTransfer();
|
||||||
document.body.appendChild(link); link.click(); document.body.removeChild(link);
|
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) {
|
function copyCodeBlock(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) {
|
|
||||||
let pre = btn.closest('.code-header').nextElementSibling;
|
let pre = btn.closest('.code-header').nextElementSibling;
|
||||||
let code = pre.querySelector('code').innerText;
|
navigator.clipboard.writeText(pre.querySelector('code').innerText);
|
||||||
navigator.clipboard.writeText(code);
|
|
||||||
btn.innerText = "✅ Copié !";
|
btn.innerText = "✅ Copié !";
|
||||||
setTimeout(() => btn.innerText = "📋 Copier", 2000);
|
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) {
|
function handleFileSelection(input) {
|
||||||
let previewList = document.getElementById('file-preview-list');
|
let previewList = document.getElementById('file-preview-list');
|
||||||
previewList.innerHTML = '';
|
previewList.innerHTML = '';
|
||||||
if (input.files) {
|
if (input.files) {
|
||||||
for (let i = 0; i < input.files.length; i++) {
|
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);
|
previewList.appendChild(pill);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoResize(textarea) {
|
function removeFile(index) {
|
||||||
if(!textarea) return;
|
let fileInput = document.getElementById('file-upload');
|
||||||
textarea.style.height = 'auto';
|
let dt = new DataTransfer();
|
||||||
textarea.style.height = textarea.scrollHeight + 'px';
|
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) {
|
function shareWhatsApp(btn) {
|
||||||
let rendered = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText;
|
let text = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText;
|
||||||
let raw = btn.closest('.ai-bubble').querySelector('.markdown-raw').innerText;
|
window.open('https://api.whatsapp.com/send?text=' + encodeURIComponent(text), '_blank');
|
||||||
let content = rendered ? rendered : raw;
|
|
||||||
window.open('https://api.whatsapp.com/send?text=' + encodeURIComponent("🤖 *Aethas38 (IA) :*\n\n" + content), '_blank');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function shareEmail(btn) {
|
function shareEmail(btn) {
|
||||||
let rendered = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText;
|
let text = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText;
|
||||||
let raw = btn.closest('.ai-bubble').querySelector('.markdown-raw').innerText;
|
window.open('mailto:?subject=Aethas38&body=' + encodeURIComponent(text), '_self');
|
||||||
let content = rendered ? rendered : raw;
|
|
||||||
window.open('mailto:?subject=Réponse Aethas38 IA&body=' + encodeURIComponent(content), '_self');
|
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
|
|
||||||
app, rt = fast_app(hdrs=theme_hdrs, secret_key="super_secret_aethas")
|
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('/')
|
@rt('/')
|
||||||
def get(session):
|
def get(session):
|
||||||
if 'current_chat_id' not in session or not session['current_chat_id']:
|
if 'current_chat_id' not in session or not session['current_chat_id']:
|
||||||
session['current_chat_id'] = str(uuid.uuid4())
|
session['current_chat_id'] = str(uuid.uuid4())
|
||||||
|
|
||||||
chat_id = session['current_chat_id']
|
chat_id = session['current_chat_id']
|
||||||
history = load_history()
|
history = load_history()
|
||||||
|
|
||||||
messages_ui = []
|
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 chat_id in history:
|
if msgs: msgs.append(Script("setTimeout(renderMarkdown, 150);"))
|
||||||
for msg in history[chat_id].get('messages', []):
|
|
||||||
if msg['role'] == 'user':
|
|
||||||
messages_ui.append(render_user_msg(msg['content']))
|
|
||||||
else:
|
|
||||||
messages_ui.append(render_ai_msg(msg['content'], msg.get('intention', ''), msg.get('fournisseur', ''), msg.get('modele', ''), msg.get('couleur', '#333')))
|
|
||||||
|
|
||||||
if messages_ui:
|
|
||||||
messages_ui.append(Script("setTimeout(renderMarkdown, 150);"))
|
|
||||||
|
|
||||||
return Title("Aethas38 - Mine_AI"), Body(
|
return Title("Aethas38 - Core"), Body(
|
||||||
Div(
|
Div(
|
||||||
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(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(
|
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"),
|
||||||
A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"),
|
|
||||||
A("💾 Exporter JSON", href="/export_json", target="_blank", cls="export-btn", title="Télécharger la conversation Globale"),
|
|
||||||
cls="nav-buttons"
|
|
||||||
),
|
|
||||||
cls="sidebar-top"
|
cls="sidebar-top"
|
||||||
),
|
),
|
||||||
render_sidebar(),
|
render_sidebar(),
|
||||||
Div(
|
Div(P("👤 Admin (Xavier)", style="color: #aaa; margin: 0; font-size: 0.9em;"), cls="sidebar-footer"),
|
||||||
P("👤 Admin (Xavier)", style="color: #aaa; margin: 0; font-size: 0.9em;"),
|
|
||||||
cls="sidebar-footer"
|
|
||||||
),
|
|
||||||
cls="sidebar"
|
cls="sidebar"
|
||||||
),
|
),
|
||||||
|
|
||||||
Div(
|
Div(
|
||||||
Div(*messages_ui, id="chat-history", cls="chat-container", style="padding-bottom: 20px;"),
|
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(
|
|
||||||
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(
|
Div(
|
||||||
Form(
|
Form(
|
||||||
Input(type="hidden", name="chat_id", value=chat_id),
|
Input(type="hidden", name="chat_id", value=chat_id),
|
||||||
Div(
|
Div(
|
||||||
Div(id="file-preview-list"),
|
Div(id="file-preview-list"),
|
||||||
Div(
|
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)"),
|
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)"),
|
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", title="Exécuter"),
|
Button("↑", type="submit", cls="btn-submit"),
|
||||||
cls="input-row"
|
cls="input-row"
|
||||||
),
|
),
|
||||||
cls="input-container-box"
|
cls="input-container-box"
|
||||||
),
|
),
|
||||||
hx_post="/chat",
|
hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader",
|
||||||
hx_target="#chat-history",
|
hx_on__after_request="this.reset(); document.getElementById('file-preview-list').innerHTML=''; document.getElementById('prompt-input').style.height='36px';",
|
||||||
hx_swap="beforeend",
|
enctype="multipart/form-data", style="width: 100%; display: flex; justify-content: center;"
|
||||||
hx_indicator="#global-loader",
|
|
||||||
hx_on__after_request="this.reset(); document.getElementById('file-preview-list').innerHTML=''; document.getElementById('prompt-input').style.height='auto';",
|
|
||||||
enctype="multipart/form-data",
|
|
||||||
style="width: 100%; display: flex; justify-content: center;"
|
|
||||||
),
|
),
|
||||||
cls="input-area"
|
cls="input-area"
|
||||||
),
|
),
|
||||||
@@ -455,188 +310,33 @@ def get(session):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# SECTION 4. LE MOTEUR DE ROUTAGE (IA + WEB + FICHIERS)
|
|
||||||
# ==========================================
|
|
||||||
@rt('/chat')
|
@rt('/chat')
|
||||||
async def post(request, session: dict):
|
async def post(request, session: dict):
|
||||||
try:
|
try:
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
prompt = form.get("prompt", "")
|
prompt = form.get("prompt", "")
|
||||||
chat_id = form.get("chat_id")
|
chat_id = form.get("chat_id", str(uuid.uuid4()))
|
||||||
files = form.getlist("files")
|
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
|
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()
|
history = load_history()
|
||||||
if chat_id in history:
|
|
||||||
history[chat_id]['title'] = nouveau_nom
|
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".
|
||||||
save_history(history)
|
Renvoie STRICTEMENT un JSON : {{"intention": "...", "fournisseur": "gemini|groq|local"}}
|
||||||
return render_sidebar()
|
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}')
|
urls = re.findall(r'https?://[^\s]+', prompt)
|
||||||
def post(chat_id: str):
|
mots_cles = ["cherche", "recherche", "sur internet", "sur le web", "sur le net", "va sur", "trouve", "site", "donne", "historique", "version"]
|
||||||
if not chat_id or chat_id == "null": return render_sidebar()
|
if any(m in prompt.lower() for m in mots_cles):
|
||||||
history = load_history()
|
urls.extend(rechercher_sur_google(prompt))
|
||||||
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}')
|
web_text = extraire_contenu_urls(list(set(urls))) if urls else ""
|
||||||
def post(chat_id: str):
|
|
||||||
if not chat_id or chat_id == "null": return render_sidebar()
|
file_summary = []
|
||||||
history = load_history()
|
file_names = []
|
||||||
if chat_id in history:
|
for f in files[:10]:
|
||||||
del history[chat_id]
|
if f && f.filename: # Wait, Python uses `and` instead of `&&`
|
||||||
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)
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from openai import OpenAI
|
||||||
|
from groq import Groq
|
||||||
|
from google import genai
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
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 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"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def nettoyer_vieilles_discussions():
|
||||||
|
history = load_history()
|
||||||
|
now = time.time()
|
||||||
|
un_an_en_secondes = 365 * 24 * 60 * 60
|
||||||
|
a_supprimer = [cid for cid, data in history.items() if not data.get('pinned', False) and now - data.get('timestamp', now) > un_an_en_secondes]
|
||||||
|
if a_supprimer:
|
||||||
|
for cid in a_supprimer:
|
||||||
|
del history[cid]
|
||||||
|
save_history(history)
|
||||||
|
print(f"🧹 AETHAS CORE : Nettoyage auto terminé. {len(a_supprimer)} discussion(s) supprimée(s).")
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
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"}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
def rechercher_sur_google(requete):
|
||||||
|
try:
|
||||||
|
url = "https://lite.duckduckgo.com/lite/"
|
||||||
|
data = {'q': requete}
|
||||||
|
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
|
||||||
|
resp = requests.post(url, data=data, headers=headers, timeout=5)
|
||||||
|
soup = BeautifulSoup(resp.text, 'html.parser')
|
||||||
|
|
||||||
|
urls = []
|
||||||
|
for a in soup.find_all('a', class_='result-link', href=True):
|
||||||
|
href = a['href']
|
||||||
|
if href.startswith('http') and 'duckduckgo' not in href:
|
||||||
|
urls.append(href)
|
||||||
|
if len(urls) >= 5:
|
||||||
|
break
|
||||||
|
if not urls:
|
||||||
|
for a in soup.find_all('a', href=True):
|
||||||
|
href = a['href']
|
||||||
|
if href.startswith('http') and 'duckduckgo' not in href and href not in urls:
|
||||||
|
urls.append(href)
|
||||||
|
if len(urls) >= 5:
|
||||||
|
break
|
||||||
|
return urls[:5]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur recherche web: {e}")
|
||||||
|
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 {url}: {e}]"
|
||||||
|
return scraped_text
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from fasthtml.common import *
|
||||||
|
from modules.database import load_history
|
||||||
|
|
||||||
|
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")
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
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"}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
def rechercher_sur_google(requete):
|
||||||
|
try:
|
||||||
|
url = f"https://lite.duckduckgo.com/lite/"
|
||||||
|
data = {'q': requete}
|
||||||
|
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
|
||||||
|
resp = requests.post(url, data=data, headers=headers, timeout=5)
|
||||||
|
soup = BeautifulSoup(resp.text, 'html.parser')
|
||||||
|
|
||||||
|
urls = []
|
||||||
|
for a in soup.find_all('a', class_='result-link', href=True):
|
||||||
|
href = a['href']
|
||||||
|
if href.startswith('http') and 'duckduckgo' not in href:
|
||||||
|
urls.append(href)
|
||||||
|
if len(urls) >= 5:
|
||||||
|
break
|
||||||
|
if not urls:
|
||||||
|
for a in soup.find_all('a', href=True):
|
||||||
|
href = a['href']
|
||||||
|
if href.startswith('http') and 'duckduckgo' not in href and href not in urls:
|
||||||
|
urls.append(href)
|
||||||
|
if len(urls) >= 5:
|
||||||
|
break
|
||||||
|
return urls[:5]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur recherche web: {e}")
|
||||||
|
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 {url}: {e}]"
|
||||||
|
return scraped_text
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# Création du dossier de rangement
|
||||||
|
os.makedirs("modules", exist_ok=True)
|
||||||
|
|
||||||
|
# 1. database.py
|
||||||
|
with open("modules/database.py", "w", encoding="utf-8") as f:
|
||||||
|
f.write('''import os
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def nettoyer_vieilles_discussions():
|
||||||
|
history = load_history()
|
||||||
|
now = time.time()
|
||||||
|
un_an_en_secondes = 365 * 24 * 60 * 60
|
||||||
|
a_supprimer = [cid for cid, data in history.items() if not data.get('pinned', False) and now - data.get('timestamp', now) > un_an_en_secondes]
|
||||||
|
if a_supprimer:
|
||||||
|
for cid in a_supprimer:
|
||||||
|
del history[cid]
|
||||||
|
save_history(history)
|
||||||
|
print(f"🧹 AETHAS CORE : Nettoyage auto terminé. {len(a_supprimer)} discussion(s) supprimée(s).")
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 2. scraper.py
|
||||||
|
with open("modules/scraper.py", "w", encoding="utf-8") as f:
|
||||||
|
f.write('''import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
def rechercher_sur_google(requete):
|
||||||
|
try:
|
||||||
|
url = "https://lite.duckduckgo.com/lite/"
|
||||||
|
data = {'q': requete}
|
||||||
|
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
|
||||||
|
resp = requests.post(url, data=data, headers=headers, timeout=5)
|
||||||
|
soup = BeautifulSoup(resp.text, 'html.parser')
|
||||||
|
|
||||||
|
urls = []
|
||||||
|
for a in soup.find_all('a', class_='result-link', href=True):
|
||||||
|
href = a['href']
|
||||||
|
if href.startswith('http') and 'duckduckgo' not in href:
|
||||||
|
urls.append(href)
|
||||||
|
if len(urls) >= 5:
|
||||||
|
break
|
||||||
|
if not urls:
|
||||||
|
for a in soup.find_all('a', href=True):
|
||||||
|
href = a['href']
|
||||||
|
if href.startswith('http') and 'duckduckgo' not in href and href not in urls:
|
||||||
|
urls.append(href)
|
||||||
|
if len(urls) >= 5:
|
||||||
|
break
|
||||||
|
return urls[:5]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur recherche web: {e}")
|
||||||
|
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 {url}: {e}]"
|
||||||
|
return scraped_text
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 3. ai_engine.py (Contient les clients et l'accès OpenAI)
|
||||||
|
with open("modules/ai_engine.py", "w", encoding="utf-8") as f:
|
||||||
|
f.write('''import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from openai import OpenAI
|
||||||
|
from groq import Groq
|
||||||
|
from google import genai
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
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 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"
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 4. router.py
|
||||||
|
with open("modules/router.py", "w", encoding="utf-8") as f:
|
||||||
|
f.write('''import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
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"}
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 5. ui_components.py (Gestion de l'affichage UI et sidebar)
|
||||||
|
with open("modules/ui_components.py", "w", encoding="utf-8") as f:
|
||||||
|
f.write('''from fasthtml.common import *
|
||||||
|
from modules.database import load_history
|
||||||
|
|
||||||
|
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")
|
||||||
|
''')
|
||||||
|
|
||||||
|
print("📁 Dossier 'modules/' et sous-scripts générés avec succès !")
|
||||||
Reference in new issue
Block a user