v3 refonte totale
Build and Push Docker Image / build-and-push (push) Failing after 40s

This commit is contained in:
xavier committed 2026-10-06 00:36:10 +02:00
1 parent 280e6e0313
commit cf9c7f7d3e
2 files changed
+265 -258

No files matched your search

+263 -257
View File
@@ -4,12 +4,15 @@ import requests
import json import json
import uuid import uuid
import asyncio import asyncio
import time
import re
from datetime import datetime from datetime import datetime
from dotenv import load_dotenv from dotenv import load_dotenv
from auth import validate_password_strength, hash_password, verify_password, generate_totp_secret, verify_totp from auth import validate_password_strength, hash_password, verify_password, generate_totp_secret, verify_totp
load_dotenv() load_dotenv()
# --- THEME & SCRIPTS ---
theme_hdrs = [ theme_hdrs = [
Script(src="https://cdn.tailwindcss.com"), Script(src="https://cdn.tailwindcss.com"),
Script(src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"), Script(src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"),
@@ -22,16 +25,15 @@ theme_hdrs = [
.chat-container { flex-grow: 1; overflow-y: auto; margin-bottom: 20px; border: 1px solid #333; padding: 15px; border-radius: 8px; background: #151e2e; } .chat-container { flex-grow: 1; overflow-y: auto; margin-bottom: 20px; border: 1px solid #333; padding: 15px; border-radius: 8px; background: #151e2e; }
.input-row { display: flex; gap: 10px; align-items: flex-end; width: 100%; background: #1e293b; padding: 10px; border-radius: 10px; border: 1px solid #333;} .input-row { display: flex; gap: 10px; align-items: flex-end; width: 100%; background: #1e293b; padding: 10px; border-radius: 10px; border: 1px solid #333;}
.input-box { flex-grow: 1; padding: 12px; border-radius: 5px; background: #2a2a35; border: 1px solid #444; color: white; outline: none; } .input-box { flex-grow: 1; padding: 12px; border-radius: 5px; background: #2a2a35; border: 1px solid #444; color: white; outline: none; }
.file-upload-btn { background: #2a2a35; border: 1px solid #444; color: #94a3b8; padding: 12px; border-radius: 5px; cursor: pointer; font-size:14px; display: flex; align-items:center;} .file-upload-btn { background: #2a2a35; border: 1px solid #444; color: #94a3b8; padding: 12px; border-radius: 5px; cursor: pointer; font-size:14px; display: flex; align-items:center; justify-content:center;}
.model-select { padding: 12px; border-radius: 5px; background: #2a2a35; border: 1px solid #444; color: #00e5ff; outline: none; font-weight: bold; cursor: pointer;} .model-selector { padding: 8px; border-radius: 5px; background: #1e293b; border: 1px solid #444; color: #00e5ff; width: 100%; font-size: 12px; outline:none; }
.send-btn { padding: 12px 24px; border-radius: 5px; background: #00e5ff; color: #0f172a; font-weight: bold; cursor: pointer; border: none; } .send-btn { padding: 12px 24px; border-radius: 5px; background: #00e5ff; color: #0f172a; font-weight: bold; cursor: pointer; border: none; }
.msg-user { color: #00e5ff; font-size: 12px; font-weight: bold; margin-bottom: 2px; margin-top: 15px;} .msg-user { color: #00e5ff; font-size: 12px; font-weight: bold; margin-bottom: 2px; margin-top: 15px;}
.msg-ia { color: #a855f7; font-size: 12px; font-weight: bold; margin-bottom: 2px; margin-top: 15px;} .msg-ia { color: #a855f7; font-size: 12px; font-weight: bold; margin-bottom: 2px; margin-top: 15px;}
.bubble-user { background: #1e293b; padding: 10px; border-radius: 5px; display: inline-block; color: white; border: 1px solid #333;} .bubble-user { background: #1e293b; padding: 10px; border-radius: 5px; display: inline-block; color: white; border: 1px solid #333;}
.bubble-ia { background: #1e293b; padding: 15px; border-radius: 8px; display: block; color: #e2e8f0; } .bubble-ia { background: #1e293b; padding: 15px; border-radius: 8px; display: block; color: #e2e8f0; border: 1px solid #333;}
.bubble-ia pre { background: #111827; padding: 15px; border-radius: 8px; margin-top: 10px; position: relative; border: 1px solid #333;} .bubble-ia pre { background: #111827; padding: 15px; border-radius: 8px; margin-top: 10px; position: relative; border: 1px solid #444;}
.context-radio-group { display: flex; gap: 15px; background: #1e293b; padding: 10px 15px; border-radius: 8px; border: 1px solid #333; margin-bottom: 10px; width: fit-content; } .htmx-indicator { display: none; position: absolute; bottom: 180px; left: 50%; transform: translateX(-50%); background: rgba(15, 23, 42, 0.9); border: 1px solid #00e5ff; padding: 15px 30px; border-radius: 50px; color: #00e5ff; font-weight: bold; z-index: 50;}
.htmx-indicator { display: none; position: absolute; bottom: 120px; left: 50%; transform: translateX(-50%); background: rgba(15, 23, 42, 0.9); border: 1px solid #00e5ff; padding: 15px 30px; border-radius: 50px; color: #00e5ff; font-weight: bold; z-index: 50;}
.htmx-request .htmx-indicator { display: block; } .htmx-request .htmx-indicator { display: block; }
.history-btn { background: #2a2a35; color: #e2e8f0; border: 1px solid #444; padding: 8px; border-radius: 5px; cursor: pointer; text-align: left; } .history-btn { background: #2a2a35; color: #e2e8f0; border: 1px solid #444; padding: 8px; border-radius: 5px; cursor: pointer; text-align: left; }
.history-btn:hover { background: #38bdf8; color: #0f172a; } .history-btn:hover { background: #38bdf8; color: #0f172a; }
@@ -39,44 +41,35 @@ theme_hdrs = [
.export-btn:hover { background: #00e5ff; color: #0f172a; } .export-btn:hover { background: #00e5ff; color: #0f172a; }
"""), """),
Script(""" Script("""
function updateUI() { function filterModels() {
let ctx = document.querySelector('input[name="context_type"]:checked').value; let filter = document.querySelector('input[name="filter_type"]:checked').value;
let modBox = document.getElementById('model-selection-area'); document.querySelectorAll('.model-selector option').forEach(opt => {
if(ctx === 'minecraft' || ctx === 'consolidated') { modBox.style.display = 'none'; } if(opt.value === "") return;
else { modBox.style.display = 'flex'; } let isFree = opt.getAttribute('data-free') === 'true';
if(filter === 'free' && !isFree) opt.style.display = 'none';
else if(filter === 'paid' && isFree) opt.style.display = 'none';
else opt.style.display = 'block';
});
}
function validateMultiIA() {
let wCount = 0;
for(let i=1; i<=5; i++) { if(document.getElementById('worker'+i).value !== "") wCount++; }
if(wCount === 0) {
alert("Erreur : Vous devez sélectionner au moins une IA de Travail.");
return false;
}
if(wCount > 1) {
if(document.getElementById('writer').value === "" || document.getElementById('synthesizer').value === "") {
alert("Erreur : Puisque vous avez sélectionné plusieurs travailleurs, le Rédacteur et le Concaténeur sont obligatoires.");
return false;
}
}
return true;
} }
function updateFileCount(input) { function updateFileCount(input) {
let label = document.getElementById('file-label-text'); let label = document.getElementById('file-label-text');
if(input && input.files && input.files.length > 0) { if(input.files && input.files.length > 0) { label.innerText = `📎 ${input.files.length} fichier(s)`; label.style.color = '#00e5ff'; }
label.innerText = `📎 ${input.files.length} fichier(s)`; else { label.innerText = '📎 Fichiers Joints'; label.style.color = '#94a3b8'; }
label.style.color = '#00e5ff';
} else {
label.innerText = '📎 Fichiers';
label.style.color = '#94a3b8';
}
}
async function downloadModels() {
let tracker = document.getElementById('loading-tracker');
let oldText = tracker.innerText;
tracker.style.display = 'block';
tracker.innerText = '⚙️ Extraction et traduction du catalogue (Patientez ~1 min)...';
try {
let res = await fetch('/export_models');
let text = await res.text();
let blob = new Blob([text], {type: 'text/plain'});
let url = window.URL.createObjectURL(blob);
let a = document.createElement('a');
a.href = url;
let d = new Date();
let ds = d.getFullYear() + ("0"+(d.getMonth()+1)).slice(-2) + ("0"+d.getDate()).slice(-2) + "_" + ("0"+d.getHours()).slice(-2) + ("0"+d.getMinutes()).slice(-2);
a.download = 'modeles_ia_' + ds + '.txt';
a.click();
window.URL.revokeObjectURL(url);
} catch(e) {
alert("Erreur lors de l'exportation.");
}
tracker.style.display = 'none';
tracker.innerText = oldText;
} }
htmx.onLoad(function(content) { htmx.onLoad(function(content) {
content.querySelectorAll('.bubble-ia:not(.rendered)').forEach(function(el) { content.querySelectorAll('.bubble-ia:not(.rendered)').forEach(function(el) {
@@ -91,6 +84,7 @@ theme_hdrs = [
app, rt = fast_app(secret_key=os.getenv("SESSION_SECRET", "super-secret-key-fallback"), hdrs=theme_hdrs) app, rt = fast_app(secret_key=os.getenv("SESSION_SECRET", "super-secret-key-fallback"), hdrs=theme_hdrs)
# --- AUTHENTIFICATION ---
@rt('/login') @rt('/login')
def get(): def get():
return Title("Connexion - AETHAS38"), Body( return Title("Connexion - AETHAS38"), Body(
@@ -129,16 +123,16 @@ def get(session):
session.clear() session.clear()
return RedirectResponse('/login') return RedirectResponse('/login')
# --- SYSTEM & LOGS ---
@rt('/check_updates') @rt('/check_updates')
def get(): def get(session):
if not session.get("authenticated"): return Span("")
try: try:
# Appel à l'API Gitea officielle pour récupérer le dernier commit
res = requests.get("https://gitea.aethas38.duckdns.org/api/v1/repos/xavier/MULTI-IA-CODAGE/commits?limit=1", timeout=3) res = requests.get("https://gitea.aethas38.duckdns.org/api/v1/repos/xavier/MULTI-IA-CODAGE/commits?limit=1", timeout=3)
if res.status_code == 200: if res.status_code == 200:
remote_commit = res.json()[0].get("sha", "")[:7] remote_commit = res.json()[0].get("sha", "")[:7]
return Span(f"🟢 Gitea Connecté (Dernier commit : {remote_commit})", style="color:#10b981;") return Span(f"🟢 Gitea Connecté (Dernier commit : {remote_commit})", style="color:#10b981;")
else: return Span("🟠 Gitea injoignable", style="color:#fbbf24;")
return Span("🟠 Gitea injoignable", style="color:#fbbf24;")
except Exception as e: except Exception as e:
return Span("⚪ Statut réseau inconnu", style="color:#94a3b8;") return Span("⚪ Statut réseau inconnu", style="color:#94a3b8;")
@@ -153,17 +147,44 @@ def log_event(session_id, category, action):
with open(LOG_FILE, "a", encoding="utf-8") as f: with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{timestamp}] [Session:{sid_short}] [{category}] {action}\n") f.write(f"[{timestamp}] [Session:{sid_short}] [{category}] {action}\n")
# --- CATALOGUE MANAGER ---
CATALOG_FILE = "catalog.json"
MODELS_DATA = []
def load_catalog():
global MODELS_DATA
if os.path.exists(CATALOG_FILE):
with open(CATALOG_FILE, "r", encoding="utf-8") as f:
try: MODELS_DATA = json.load(f)
except: MODELS_DATA = []
if not MODELS_DATA:
# Fallback de sécurité si aucun catalogue n'a été importé
MODELS_DATA = [
{"id": "groq|llama3-8b-8192", "name": "Groq - Llama 3 (8B)", "is_free": True, "provider": "Général / Texte", "description": "Modèle par défaut."},
{"id": "gemini|gemini-1.5-flash", "name": "Google - Gemini 1.5 Flash", "is_free": True, "provider": "Vision", "description": "Modèle par défaut."}
]
load_catalog()
def save_catalog(data):
global MODELS_DATA
MODELS_DATA = data
with open(CATALOG_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
def get_budget():
mgmt_key = os.getenv("OPENROUTER_MANAGEMENT_KEY")
if not mgmt_key: return "Clé manquante"
try:
data = requests.get("https://openrouter.ai/api/v1/credits", headers={"Authorization": f"Bearer {mgmt_key}"}, timeout=5).json().get("data", {})
return f"{data.get('total_credits', 0) - data.get('total_usage', 0):.4f} $"
except: return "Erreur réseau"
# --- PERSISTENCE HISTORIQUE ---
HISTORY_FILE = "history.json" HISTORY_FILE = "history.json"
def load_index(): def load_index():
if not os.path.exists(HISTORY_FILE): return {} if not os.path.exists(HISTORY_FILE): return {}
with open(HISTORY_FILE, "r", encoding="utf-8") as f: with open(HISTORY_FILE, "r", encoding="utf-8") as f:
try: try: return json.load(f)
idx = json.load(f)
for sid, data in list(idx.items()):
if "messages" in data:
save_session_data(sid, data["messages"])
del data["messages"]
return idx
except: return {} except: return {}
def save_index(idx): def save_index(idx):
with open(HISTORY_FILE, "w", encoding="utf-8") as f: with open(HISTORY_FILE, "w", encoding="utf-8") as f:
@@ -180,256 +201,216 @@ def save_session_data(session_id, messages):
def create_new_session(): def create_new_session():
session_id = str(uuid.uuid4()) session_id = str(uuid.uuid4())
idx = load_index() idx = load_index()
idx[session_id] = {"title": f"Discussion du {datetime.now().strftime('%d/%m %H:%M')}", "pinned": False} idx[session_id] = {"title": f"Projet du {datetime.now().strftime('%d/%m %H:%M')}", "pinned": False}
save_index(idx) save_index(idx)
save_session_data(session_id, []) save_session_data(session_id, [])
log_event(session_id, "USER_ACTION", "Nouvelle session initialisée.")
return session_id return session_id
def render_history_list(): def render_history_list():
idx = load_index() idx = load_index()
sorted_sessions = sorted(idx.items(), key=lambda x: (not x[1].get('pinned', False), x[1]['title']), reverse=False) sorted_sessions = sorted(idx.items(), key=lambda x: (not x[1].get('pinned', False), x[1]['title']), reverse=False)
items = [Button("➕ Nouvelle Discussion", cls="history-btn w-full mb-3 text-center", style="background:#00e5ff; color:#0f172a; font-weight:bold;", hx_get="/session/new", hx_target="#chat-history")] items = [Button("➕ Nouveau Projet", cls="export-btn mb-3", style="background:#10b981; color:#0f172a;", hx_get="/session/new", hx_target="#chat-history")]
for sid, data in sorted_sessions: for sid, data in sorted_sessions:
pin_icon, pin_color = ("📍", "#10b981") if data.get('pinned') else ("📌", "#94a3b8") pin_icon, pin_color = ("📍", "#10b981") if data.get('pinned') else ("📌", "#94a3b8")
item = Div( item = Div(
Div(data['title'], cls="truncate flex-grow cursor-pointer hover:text-cyan-400", hx_get=f"/session/load/{sid}", hx_target="#chat-history"), Div(data['title'], cls="truncate flex-grow cursor-pointer text-sm hover:text-cyan-400", hx_get=f"/session/load/{sid}", hx_target="#chat-history"),
Div( Div(
Button("✏️️", cls="text-xs mx-1 hover:text-white", onclick=f"let name = prompt('Nouveau nom:'); if(name) {{ htmx.ajax('POST', '/history/rename/{sid}', {{values: {{title: name}}, target: '#history-list-container'}}); }}"), Button("✏", cls="text-xs mx-1 hover:text-white", onclick=f"let name = prompt('Nouveau nom:'); if(name) {{ htmx.ajax('POST', '/history/rename/{sid}', {{values: {{title: name}}, target: '#history-list-container'}}); }}"),
Button(pin_icon, style=f"color:{pin_color};", cls="text-xs mx-1 hover:text-white", hx_post=f"/history/pin/{sid}", hx_target="#history-list-container"), Button(pin_icon, style=f"color:{pin_color};", cls="text-xs mx-1 hover:text-white", hx_post=f"/history/pin/{sid}", hx_target="#history-list-container"),
Button("🗑️", cls="text-xs hover:text-red-500", onclick=f"if(confirm('Supprimer définitivement cette discussion ?')) {{ htmx.ajax('POST', '/history/delete/{sid}', {{target: '#history-list-container'}}); }}"), Button("🗑️", cls="text-xs hover:text-red-500", onclick=f"if(confirm('Supprimer ce projet ?')) {{ htmx.ajax('POST', '/history/delete/{sid}', {{target: '#history-list-container'}}); }}"),
cls="flex-shrink-0" cls="flex-shrink-0"
), ),
cls="flex justify-between items-center history-btn w-full mb-1" cls="flex justify-between items-center w-full mb-2 bg-[#2a2a35] p-2 rounded border border-[#444]"
) )
items.append(item) items.append(item)
return Div(*items, id="history-list-container", hx_swap_oob="true") return Div(*items, id="history-list-container", hx_swap_oob="true")
MODELS_DATA = [] # --- UI COMPONENTS ---
def init_models(): def render_model_dropdown(field_id, placeholder):
global MODELS_DATA groups = {}
MODELS_DATA = [ for m in MODELS_DATA:
{"id": "groq|llama3-8b-8192", "name": "Groq - Llama 3 (8B)", "is_free": True, "description": "Modèle ultra-rapide hébergé par Groq.", "architecture": {"modality": "text"}}, prov = m.get('provider', 'Général')
{"id": "gemini|gemini-1.5-flash", "name": "Google - Gemini 1.5 Flash", "is_free": True, "description": "Modèle multimodal léger de Google.", "architecture": {"modality": "text/vision"}}, if prov not in groups: groups[prov] = []
{"id": "deepseek|deepseek-coder", "name": "DeepSeek - Coder", "is_free": False, "description": "Génération de code complexe.", "architecture": {"modality": "text/code"}} groups[prov].append(m)
]
try: opts = [Option("--- Laisser vide ---", value="")]
res = requests.get("https://openrouter.ai/api/v1/models", timeout=5) for prov in sorted(groups.keys()):
if res.status_code == 200: grp_opts = []
for m in res.json().get("data", []): for m in sorted(groups[prov], key=lambda x: x['name']):
m["id"], m["name"] = f"openrouter|{m['id']}", f"OR - {m['name']}" is_free = str(m.get('is_free', False)).lower()
MODELS_DATA.append(m) label = f"{m['name']} ({'Gratuit' if is_free=='true' else 'Payant'})"
except Exception as e: log_event("system", "ERROR", f"Erreur catalogue OR : {e}") grp_opts.append(Option(label, value=m['id'], **{"data-free": is_free}))
init_models() opts.append(Optgroup(label=prov)(*grp_opts))
def get_model_options(filter_type="all"):
options = [] return Div(
for m in sorted(MODELS_DATA, key=lambda x: x['name']): P(placeholder, style="color:#94a3b8; font-size:10px; font-weight:bold; margin-bottom:2px;"),
is_free = m.get("is_free", "free" in m.get("id", "").lower()) Select(*opts, name=field_id, id=field_id, cls="model-selector", onchange="filterModels()"),
if filter_type == "free" and not is_free: continue style="margin-bottom: 8px;"
if filter_type == "paid" and is_free: continue )
options.append(Option(f"{m['name']} ({'Gratuit' if is_free else 'Payant'})", value=m['id']))
return Select(*options, name="model_id", cls="model-select", style="width: 100%;")
def get_budget(): def parse_txt_catalog(content: str):
mgmt_key = os.getenv("OPENROUTER_MANAGEMENT_KEY") """ Logique VBA traduite en Python pour le fallback du fichier TXT """
if not mgmt_key: return "Clé manquante" lines = content.split('\n')
try: models = []
data = requests.get("https://openrouter.ai/api/v1/credits", headers={"Authorization": f"Bearer {mgmt_key}"}, timeout=5).json().get("data", {}) current = {}
return f"{data.get('total_credits', 0) - data.get('total_usage', 0):.4f} $" ext_date = "Inconnue"
except: return "Erreur lecture" ext_time = ""
for line in lines:
line_trim = line.strip()
# Extraction de la date comme dans la macro
if "Généré le" in line_trim:
match = re.search(r"Généré le (\d{2}/\d{2}/\d{4}) à (\d{2}:\d{2}:\d{2})", line_trim)
if match:
ext_date = match.group(1)
ext_time = match.group(2)
if line_trim.startswith("Nom : "): current['name'] = line_trim.replace("Nom : ", "").strip()
elif line_trim.startswith("ID : "): current['id'] = line_trim.replace("ID : ", "").strip()
elif line_trim.startswith("Tarif : "): current['is_free'] = ("Gratuit" in line_trim)
elif line_trim.startswith("Spécialité : "): current['provider'] = line_trim.replace("Spécialité : ", "").strip()
elif line_trim.startswith("Description : "): current['description'] = line_trim.replace("Description : ", "").strip()
elif line_trim.startswith("---"):
if 'id' in current and 'name' in current: models.append(current)
current = {}
return models, ext_date, ext_time
def call_ollama(model_name, prompt_text): # --- API DISTANTE ---
try: def ask_llm(session_id, full_id, msg):
res = requests.post( parts = full_id.split("|", 1)
"http://host.docker.internal:11434/api/generate", provider = parts[0]
json={"model": model_name, "prompt": prompt_text, "stream": False}, actual_model = parts[1] if len(parts) > 1 else full_id
timeout=None
) log_event(session_id, "API_REQ", f"Provider: {provider} | Modèle: {actual_model}")
data = res.json()
if "response" in data:
return data["response"]
elif "error" in data:
return f"⚠️ Erreur Ollama : {data['error']}"
else:
return f"⚠️ Réponse Ollama inattendue : {str(data)}"
except Exception as e:
return f"⚠️ Erreur de connexion à Ollama : {str(e)}"
def handle_api_error(session_id, provider, raw_error):
log_event(session_id, "ERROR", f"API {provider} : {raw_error}")
sys_prompt = "Tu es un assistant technique. Traduis cette erreur d'API en français."
qwen_res = call_ollama("qwen2.5-coder:3b", f"{sys_prompt}\n\nErreur ({provider}): {raw_error}")
return f"⚠️ **Alerte Serveur ({provider})**\n{qwen_res}"
async def async_ask_llm(session_id, provider, actual_model, msg):
return await asyncio.to_thread(ask_llm, session_id, provider, actual_model, msg)
def ask_llm(session_id, provider, actual_model, msg):
log_event(session_id, "API_REQ", f"Interrogation : {provider} | Modèle : {actual_model}")
try: try:
if provider == "openrouter": if provider == "openrouter":
res = requests.post("https://openrouter.ai/api/v1/chat/completions", headers={"Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}"}, json={"model": actual_model, "messages": [{"role": "user", "content": msg}]}) res = requests.post("https://openrouter.ai/api/v1/chat/completions", headers={"Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}"}, json={"model": actual_model, "messages": [{"role": "user", "content": msg}]})
if res.status_code != 200: return handle_api_error(session_id, provider, res.text) if res.status_code == 200: return res.json()["choices"][0]["message"]["content"]
return res.json()["choices"][0]["message"]["content"] return f"⚠️ Erreur API ({provider}) : {res.text}"
elif provider == "groq": elif provider == "groq":
res = requests.post("https://api.groq.com/openai/v1/chat/completions", headers={"Authorization": f"Bearer {os.getenv('GROQ_API_KEY')}"}, json={"model": actual_model, "messages": [{"role": "user", "content": msg}]}) res = requests.post("https://api.groq.com/openai/v1/chat/completions", headers={"Authorization": f"Bearer {os.getenv('GROQ_API_KEY')}"}, json={"model": actual_model, "messages": [{"role": "user", "content": msg}]})
if res.status_code != 200: return handle_api_error(session_id, provider, res.text) if res.status_code == 200: return res.json()["choices"][0]["message"]["content"]
return res.json()["choices"][0]["message"]["content"] return f"⚠️ Erreur API ({provider}) : {res.text}"
elif provider == "gemini": elif provider == "gemini":
res = requests.post(f"https://generativelanguage.googleapis.com/v1beta/models/{actual_model}:generateContent?key={os.getenv('GEMINI_API_KEY')}", json={"contents": [{"parts": [{"text": msg}]}]}) res = requests.post(f"https://generativelanguage.googleapis.com/v1beta/models/{actual_model}:generateContent?key={os.getenv('GEMINI_API_KEY')}", json={"contents": [{"parts": [{"text": msg}]}]})
if res.status_code != 200: return handle_api_error(session_id, provider, res.text) if res.status_code == 200: return res.json()["candidates"][0]["content"]["parts"][0]["text"]
return res.json()["candidates"][0]["content"]["parts"][0]["text"] return f"⚠️ Erreur API ({provider}) : {res.text}"
return handle_api_error(session_id, provider, "Fournisseur non implémenté.") return "⚠️ Fournisseur API non implémenté ou ID invalide."
except Exception as e: return handle_api_error(session_id, provider, str(e)) except Exception as e: return f"⚠️ Erreur de connexion : {str(e)}"
def pipeline_minecraft(session_id, raw_msg): async def async_ask_llm(session_id, full_id, msg):
log_event(session_id, "PIPELINE", "Local Minecraft") return await asyncio.to_thread(ask_llm, session_id, full_id, msg)
try:
en_prompt = call_ollama("qwen2.5-coder:3b", f"Translate this Minecraft modding request into technical English. Return ONLY English text.\n\n{raw_msg}")
phuzzy_res = call_ollama("phuzzy:latest", en_prompt)
final_fr = call_ollama("qwen2.5-coder:3b", f"Traduis les explications techniques en français. NE TRADUIS PAS le code.\n\n{phuzzy_res}")
return final_fr, "Qwen ➔ Phuzzy ➔ Qwen"
except Exception as e: return handle_api_error(session_id, "Ollama Local", str(e)), "Erreur Locale"
async def pipeline_consolidated(session_id, raw_msg): # --- ROUTES & VIEWS ---
log_event(session_id, "PIPELINE", "Consolidation Multi-IA") SESSION_PROGRESS = {}
try:
opt_prompt = call_ollama("qwen2.5-coder:3b", f"Optimise cette demande pour des LLM codeurs. Sois ultra précis.\n\n{raw_msg}")
rep_groq, rep_gemini = await asyncio.gather(
async_ask_llm(session_id, "groq", "llama3-8b-8192", opt_prompt),
async_ask_llm(session_id, "gemini", "gemini-1.5-flash", opt_prompt)
)
sys_synth = "Tu es un Architecte Logiciel Senior. Voici la même demande traitée par deux IA différentes. Lis leurs propositions, corrige les erreurs potentielles, garde le meilleur des deux, et génère le code final absolu et parfait en français."
final_prompt = f"{sys_synth}\n\n--- IA 1 (Groq) ---\n{rep_groq}\n\n--- IA 2 (Gemini) ---\n{rep_gemini}"
final_res = call_ollama("qwen2.5-coder:3b", final_prompt)
return final_res, "Qwen ➔ [Groq + Gemini] ➔ Synthèse Qwen"
except Exception as e: return handle_api_error(session_id, "Pipeline Multi-IA", str(e)), "Erreur Consolidation"
@rt('/session/new') @rt('/status/{sid}')
def get(session): def get_status(sid: str):
if not session.get("authenticated"): return RedirectResponse('/login') return SESSION_PROGRESS.get(sid, "⚙️ Traitement en cours...")
sid = create_new_session()
return Div(), Input(type="hidden", name="session_id", value=sid, id="current-session-id", hx_swap_oob="true"), render_history_list()
@rt('/session/load/{sid}')
def get(sid: str, session):
if not session.get("authenticated"): return RedirectResponse('/login')
log_event(sid, "USER_ACTION", "Chargement de la session.")
messages = load_session_data(sid)
bubbles = [Div(P("Vous", cls="msg-user"), Div(NotStr(f"{msg['user']}{msg.get('info_fichiers', '')}{msg.get('info_local', '')}"), cls="bubble-user"), P(f"AETHAS38 ({msg.get('nom_affichage', 'IA')})", cls="msg-ia"), Div(msg['ia'], cls="bubble-ia")) for msg in messages]
return tuple(bubbles) + (Input(type="hidden", name="session_id", value=sid, id="current-session-id", hx_swap_oob="true"),)
@rt('/history/pin/{sid}')
def post_pin(sid: str, session):
if not session.get("authenticated"): return RedirectResponse('/login')
idx = load_index()
if sid in idx:
idx[sid]['pinned'] = not idx[sid].get('pinned', False)
save_index(idx)
log_event(sid, "USER_ACTION", "Épinglage modifié.")
return render_history_list()
@rt('/history/rename/{sid}')
def post_rename(sid: str, title: str, session):
if not session.get("authenticated"): return RedirectResponse('/login')
idx = load_index()
if sid in idx:
idx[sid]['title'] = title
save_index(idx)
log_event(sid, "USER_ACTION", f"Renommé en: {title}")
return render_history_list()
@rt('/history/delete/{sid}')
def post_delete(sid: str, session):
if not session.get("authenticated"): return RedirectResponse('/login')
idx = load_index()
if sid in idx:
del idx[sid]
save_index(idx)
if os.path.exists(f"sessions/{sid}.json"): os.remove(f"sessions/{sid}.json")
log_event(sid, "USER_ACTION", "Session supprimée.")
return render_history_list()
@rt('/export_models')
def get(session):
if not session.get("authenticated"): return RedirectResponse('/login')
log_event("system", "SYSTEM", "Export catalogue.")
txt_content = f"=== CATALOGUE ({datetime.now().strftime('%d/%m/%Y %H:%M:%S')}) ===\n\n"
for m in MODELS_DATA: txt_content += f"Nom : {m.get('name')}\nID : {m.get('id')}\nDescription : {str(m.get('description', ''))[:150]}\n{'-'*60}\n"
return Response(txt_content, media_type="text/plain")
@rt('/') @rt('/')
def get(session): def get(session):
if not session.get("authenticated"): return RedirectResponse('/login') if not session.get("authenticated"): return RedirectResponse('/login')
session_id = create_new_session() session_id = create_new_session()
return Title("AETHAS38 Multi-IA"), Body( return Title("AETHAS38 Orchestrateur"), Body(
Div( Div(
# --- SIDEBAR GAUCHE ---
Div( Div(
H2("AETHAS 38", style="color:#00e5ff; font-weight:bold; font-size:24px;"), H2("AETHAS 38", style="color:#00e5ff; font-weight:bold; font-size:24px;"),
P("Tech Core - Multi IA", style="color:#a855f7; margin-bottom: 20px;"), P("Orchestrateur Distant", style="color:#a855f7; margin-bottom: 10px; font-size:12px;"),
Div("⏳ Vérification GitHub...", hx_get="/check_updates", hx_trigger="load", style="font-size:10px; margin-bottom:15px;"),
Div( Div(
P("STATUT SYSTÈME", style="color:#94a3b8; font-size:10px; font-weight:bold; margin-bottom:5px;"), H3("Importer Nouveaux Modèles", style="color:#94a3b8; font-size:12px; font-weight:bold; margin-bottom:5px;"),
Div("⏳ Vérification GitHub...", hx_get="/check_updates", hx_trigger="load", style="font-size:12px; font-weight:bold; margin-bottom:15px;"), Form(
Input(type="file", name="catalog_file", accept=".txt,.xlsm,.xlsx", required=True, id="cat-upload", style="display:none;", onchange="document.getElementById('cat-label').innerText = '📎 Fichier Sélectionné';"),
Label(Span("📎 Sélectionner (.txt ou .xlsm)", id="cat-label"), _for="cat-upload", cls="file-upload-btn mb-2"),
Button("Importer le Catalogue", type="submit", cls="export-btn", style="background:#a855f7; color:#fff; border:none;"),
hx_post="/import_catalog", hx_target="#catalog-status", enctype="multipart/form-data"
),
Div(id="catalog-status", style="font-size:10px; color:#10b981; margin-top:5px; text-align:center;"),
style="background:#151e2e; padding:10px; border-radius:5px; border:1px solid #333; margin-bottom:15px;"
), ),
Div( Div(H3("Historique", style="color:#94a3b8; font-size:12px; font-weight:bold; margin-bottom:10px;"), render_history_list(), style="flex-grow:1; overflow:hidden;"),
H3("Historique de Code", style="color:#94a3b8; font-size:12px; font-weight:bold; margin-bottom:10px;"),
render_history_list(), Button("📥 Exporter Modèles Actuels (.txt)", type="button", cls="export-btn", onclick="window.location.href='/export_models'"),
style="display:flex; flex-direction:column; flex-grow:1; margin-bottom: 10px; overflow:hidden;"
),
Button("📥 Exporter Modèles (.txt)", type="button", cls="export-btn w-full mt-2", onclick="downloadModels()"),
A("Déconnexion", href="/logout", cls="export-btn w-full mt-2", style="color:#ef4444; border-color:#ef4444; margin-bottom:10px;"), A("Déconnexion", href="/logout", cls="export-btn w-full mt-2", style="color:#ef4444; border-color:#ef4444; margin-bottom:10px;"),
Div( Div(P("BUDGET", style="color:#94a3b8; font-size:10px; font-weight:bold;"), P(get_budget(), id="budget-display", style="color:#10b981; font-size:16px; font-weight:bold;")),
P("BUDGET OPENROUTER", style="color:#94a3b8; font-size:10px; font-weight:bold; margin-bottom:5px;"),
P(get_budget(), id="budget-display", style="color:#10b981; font-size:18px; font-weight:bold;"),
cls="budget-box"
),
cls="sidebar" cls="sidebar"
), ),
# --- ZONE PRINCIPALE ---
Div( Div(
Div(id="chat-history", cls="chat-container"), Div(id="chat-history", cls="chat-container"),
Div("⚙️ Traitement de l'Architecture IA en cours...", id="loading-tracker", cls="htmx-indicator"), Div(Span("⚙️ Préparation du Pipeline...", id="loading-text"), id="loading-tracker", cls="htmx-indicator"),
Form( Form(
Input(type="hidden", name="session_id", value=session_id, id="current-session-id"), Input(type="hidden", name="session_id", value=session_id, id="current-session-id"),
Div( Div(
Label(Input(type="radio", name="context_type", value="generic", checked=True, onchange="updateUI()"), " 💻 Code Générique"), P("Filtre d'affichage :", style="color:#00e5ff; font-weight:bold; font-size:12px; margin-right:15px;"),
Label(Input(type="radio", name="context_type", value="consolidated", onchange="updateUI()"), " 🧠 Consolidation"), Label(Input(type="radio", name="filter_type", value="all", checked=True, onchange="filterModels()"), " Tous"),
Label(Input(type="radio", name="context_type", value="minecraft", onchange="updateUI()"), " ⛏ Minecraft"), Label(Input(type="radio", name="filter_type", value="free", onchange="filterModels()"), " Gratuits Uniquement"),
cls="context-radio-group text-sm text-white flex gap-4" Label(Input(type="radio", name="filter_type", value="paid", onchange="filterModels()"), " Payants Uniquement"),
cls="flex items-center text-sm text-white gap-4 mb-4 bg-[#1e293b] p-2 rounded border border-[#333] w-fit"
), ),
Div( Div(
Select(Option("Tous les modèles", value="all"), Option("Gratuits", value="free"), Option("Payants", value="paid"), name="filter_type", cls="model-select", hx_get="/filter_models", hx_target="#model-select-wrapper", style="width: 250px;"), Div(render_model_dropdown("writer", "1. Rédacteur de Prompt"), render_model_dropdown("synthesizer", "7. Concaténeur Final"), style="border-right: 1px solid #444; padding-right:15px;"),
Div(get_model_options("all"), id="model-select-wrapper", style="flex-grow: 1;"), Div(
cls="flex gap-2 mb-2 w-full", id="model-selection-area" render_model_dropdown("worker1", "2. IA Travailleur 1 (Obligatoire)"),
render_model_dropdown("worker2", "3. IA Travailleur 2"),
render_model_dropdown("worker3", "4. IA Travailleur 3"),
style="padding: 0 15px;"
),
Div(
render_model_dropdown("worker4", "5. IA Travailleur 4"),
render_model_dropdown("worker5", "6. IA Travailleur 5"),
style="padding-left: 15px;"
),
cls="grid grid-cols-3 gap-4 mb-4 bg-[#151e2e] p-4 rounded border border-[#333]"
), ),
Div( Div(
Input(type="file", name="fichiers", id="file-upload", multiple=True, style="display:none;", onchange="updateFileCount(this)"), Input(type="file", name="fichiers", id="file-upload", multiple=True, style="display:none;", onchange="updateFileCount(this)"),
Label(Span("📎 Fichiers", id="file-label-text"), _for="file-upload", cls="file-upload-btn"), Label(Span("📎 Fichiers Joints", id="file-label-text"), _for="file-upload", cls="file-upload-btn w-40"),
Input(type="text", name="msg", placeholder="Insérez votre requête...", cls="input-box", required=True), Input(type="text", name="msg", placeholder="Détaillez le travail à effectuer...", cls="input-box", required=True),
Button("Envoyer", type="submit", cls="send-btn"), Button("Lancer le Pipeline", type="submit", cls="send-btn"),
cls="input-row" cls="input-row"
), ),
hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#loading-tracker", enctype="multipart/form-data", hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#loading-tracker", enctype="multipart/form-data",
**{"hx-on:htmx:afterRequest": "if(event.detail.successful) { this.reset(); updateFileCount(document.getElementById('file-upload')); }"} onsubmit="return validateMultiIA();",
**{"hx-on:htmx:afterRequest": "if(event.detail.successful) { this.reset(); document.getElementById('file-label-text').innerText='📎 Fichiers Joints'; filterModels(); }"}
), ),
cls="main-content" cls="main-content"
), ),
style="display: grid; grid-template-columns: 250px 1fr;" style="display: grid; grid-template-columns: 280px 1fr;"
) ),
Script("""
let statusInterval;
document.addEventListener('htmx:beforeRequest', function(evt) {
if(evt.detail.target.id === 'chat-history') {
let sid = document.getElementById('current-session-id').value;
document.getElementById('loading-text').innerText = "⚙️ Envoi des données...";
statusInterval = setInterval(async () => {
try { let res = await fetch('/status/' + sid); if(res.ok) document.getElementById('loading-text').innerText = await res.text(); } catch(e) {}
}, 800);
}
});
document.addEventListener('htmx:afterRequest', function(evt) { if(evt.detail.target.id === 'chat-history') clearInterval(statusInterval); });
filterModels();
""")
) )
@rt('/filter_models') # --- PIPELINE DE TRAITEMENT ---
def get(filter_type: str, session):
if not session.get("authenticated"): return RedirectResponse('/login')
return get_model_options(filter_type)
@rt('/chat') @rt('/chat')
async def post(msg: str, model_id: str, session_id: str, session, context_type: str = "generic", fichiers: list[UploadFile] = None): async def post(msg: str, session_id: str, session, writer: str = "", worker1: str = "", worker2: str = "", worker3: str = "", worker4: str = "", worker5: str = "", synthesizer: str = "", fichiers: list[UploadFile] = None):
if not session.get("authenticated"): return RedirectResponse('/login') if not session.get("authenticated"): return RedirectResponse('/login')
log_event(session_id, "USER_ACTION", f"Message ({context_type})") t_start = time.time()
# 1. Préparation
files_context, noms_fichiers = "", [] files_context, noms_fichiers = "", []
if fichiers: if fichiers:
for f in fichiers: for f in fichiers:
@@ -438,31 +419,56 @@ async def post(msg: str, model_id: str, session_id: str, session, context_type:
try: files_context += f"\n--- {f.filename} ---\n{(await f.read()).decode('utf-8')}\n" try: files_context += f"\n--- {f.filename} ---\n{(await f.read()).decode('utf-8')}\n"
except: files_context += f"\n--- {f.filename} (Binaire ignoré) ---\n" except: files_context += f"\n--- {f.filename} (Binaire ignoré) ---\n"
full_req = f"Fichiers fournis:\n{files_context}\nDemande: {msg}" if files_context else msg base_prompt = f"Fichiers fournis :\n{files_context}\n\nConsigne de l'utilisateur : {msg}" if files_context else msg
workers = [w for w in [worker1, worker2, worker3, worker4, worker5] if w]
if context_type == "minecraft":
ia_reponse, pipeline_info = pipeline_minecraft(session_id, full_req) # 2. Phase Rédacteur (Optionnel)
nom_affichage = "Local - Phuzzy/Minecraft" t_w0 = time.time()
elif context_type == "consolidated": if writer:
ia_reponse, pipeline_info = await pipeline_consolidated(session_id, full_req) SESSION_PROGRESS[session_id] = "🧠 Étape 1 : Le Rédacteur prépare l'invite..."
nom_affichage = "Qwen Synthèse (via Groq/Gemini)" prompt_to_work = ask_llm(session_id, writer, "Tu es un Ingénieur Prompt Senior. Reformule et optimise techniquement cette demande pour des LLM spécialisés. Retourne uniquement l'invite optimisée sans salutations :\n\n" + base_prompt)
else: else:
opt_prompt = call_ollama("qwen2.5-coder:3b", f"Optimise cette demande pour un LLM codeur. Retourne UNIQUEMENT le prompt.\n\n{full_req}") prompt_to_work = base_prompt
parts = model_id.split("|", 1) t_w1 = time.time()
provider, actual_model = parts[0], parts[1] if len(parts) > 1 else model_id
ia_reponse = ask_llm(session_id, provider, actual_model, opt_prompt)
nom_affichage = f"{provider.capitalize()} - {actual_model.split('/')[-1]}"
pipeline_info = "Qwen ➔ API Distante Unique"
info_fichiers = f"<br><span style='color:#a855f7; font-size:10px;'>📎 {len(noms_fichiers)} fichier(s)</span>" if noms_fichiers else "" # 3. Phase Travailleurs (Parallèle)
info_local = f"<br><span style='color:#38bdf8; font-size:10px;'>⚙️ Pipeline : {pipeline_info}</span>" SESSION_PROGRESS[session_id] = f"⚡ Étape 2 : Travail en cours ({len(workers)} IA en parallèle)..."
tasks = [async_ask_llm(session_id, w, prompt_to_work) for w in workers]
results = await asyncio.gather(*tasks)
t_w2 = time.time()
# 4. Phase Concaténeur
if synthesizer and len(workers) > 0:
SESSION_PROGRESS[session_id] = "🏗️ Étape 3 : Synthèse et assemblage final..."
synth_input = "Tu es l'Architecte Final. Voici la même tâche effectuée par plusieurs intelligences artificielles :\n\n"
for i, res in enumerate(results): synth_input += f"--- Proposition IA {i+1} ---\n{res}\n\n"
synth_input += "Analyse ces propositions, garde le meilleur code, corrige les erreurs potentielles et génère la solution finale absolue et complète en français."
final_response = ask_llm(session_id, synthesizer, synth_input)
else:
final_response = results[0]
t_w3 = time.time()
SESSION_PROGRESS[session_id] = "✅ Pipeline Terminé"
# Rapport Chrono
rapport = (
f"\n\n---\n⏱️ **Rapport de Performance (Pipeline Distant)**\n"
f"- **Rédacteur** : {t_w1 - t_w0:.2f} s\n"
f"- **Travailleurs (Parallèle)** : {t_w2 - t_w1:.2f} s\n"
f"- **Concaténeur** : {t_w3 - t_w2:.2f} s\n"
f"- **Temps Total du Cycle** : {t_w3 - t_start:.2f} s"
)
info_lbl = f"<br><span style='color:#38bdf8; font-size:10px;'>⚙️ {len(workers)} Travailleur(s) | 📎 {len(noms_fichiers)} fichier(s)</span>"
session_data = load_session_data(session_id) session_data = load_session_data(session_id)
session_data.append({"user": msg, "ia": ia_reponse, "nom_affichage": nom_affichage, "info_fichiers": info_fichiers, "info_local": info_local}) session_data.append({"user": msg, "ia": final_response + rapport, "info": info_lbl})
save_session_data(session_id, session_data) save_session_data(session_id, session_data)
chat_bubble = Div(P("Vous", cls="msg-user"), Div(NotStr(f"{msg}{info_fichiers}{info_local}"), cls="bubble-user"), P(f"AETHAS38 ({nom_affichage})", cls="msg-ia"), Div(ia_reponse, cls="bubble-ia")) return Div(P("Vous", cls="msg-user"), Div(NotStr(f"{msg}{info_lbl}"), cls="bubble-user"), P("AETHAS38", cls="msg-ia"), Div(final_response + rapport, cls="bubble-ia")), P(get_budget(), id="budget-display", style="color:#10b981; font-size:16px; font-weight:bold;", hx_swap_oob="true")
return chat_bubble, P(get_budget(), id="budget-display", style="color:#10b981; font-size:18px; font-weight:bold;", hx_swap_oob="true")
if __name__ == '__main__': # --- IMPORT / EXPORT CATALOGUE ---
serve(port=5001) @rt('/export_models')
def get(session):
if not session.get("authenticated"): return RedirectResponse('/login')
txt_content = f"=== CATALOGUE ({datetime.now().strftime('%d/%m/%Y %H:%M:%S')}) ===\n\n"
for m in MODELS_DATA: txt_content += f"Nom : {m.get('name')}\nID : {m.get('id')}\nTarif :
+2 -1
View File
@@ -3,4 +3,5 @@ requests
python-dotenv python-dotenv
pyotp pyotp
qrcode qrcode
psutil psutil
openyxl