MAJ majeur
Build and Push Docker Image / build-and-push (push) Failing after 27s

This commit is contained in:
xavier committed 2026-10-06 13:59:17 +02:00
1 parent 626ec138e4
commit d6a7d10e3a
8 files changed
+529 -236

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
# ai_engine.py
import os
import requests
import asyncio
from datetime import datetime
os.makedirs("logs", exist_ok=True)
APP_LAUNCH_TIME = datetime.now().strftime("%Y%m%d_%H%M%S")
LOG_FILE = f"logs/AETHAS38_Run_{APP_LAUNCH_TIME}.log"
def log_event(session_id, category, action):
sid_short = session_id[:8] if session_id and session_id != "system" else "SYSTEM"
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{timestamp}] [Session:{sid_short}] [{category}] {action}\n")[cite: 11]
def ask_llm(session_id, full_id, msg):
parts = full_id.split("|", 1)
provider, actual_model = parts[0], parts[1] if len(parts) > 1 else full_id
log_event(session_id, "API_REQ", f"Provider: {provider} | Modèle: {actual_model}")[cite: 11]
try:
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}]})
if res.status_code == 200: return res.json()["choices"][0]["message"]["content"]
return f"⚠️ Erreur API ({provider}) : {res.text}"[cite: 11]
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}]})
if res.status_code == 200: return res.json()["choices"][0]["message"]["content"]
return f"⚠️ Erreur API ({provider}) : {res.text}"[cite: 11]
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}]}]})
if res.status_code == 200: return res.json()["candidates"][0]["content"]["parts"][0]["text"]
return f"⚠️ Erreur API ({provider}) : {res.text}"[cite: 11]
return "⚠️ Fournisseur API non implémenté ou ID invalide[cite: 11]."
except Exception as e: return f"⚠ Erreur de connexion : {str(e)}"[cite: 11]
async def async_ask_llm(session_id, full_id, msg):
return await asyncio.to_thread(ask_llm, session_id, full_id, msg)[cite: 11]
+56
View File
@@ -0,0 +1,56 @@
# auth.py
import re
import pyotp
import hashlib
import hmac
import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
def validate_password_strength(password):
if len(password) < 12: return "Le mot de passe doit contenir au moins 12 caractères."
if not re.search(r"[A-Z]", password): return "Le mot de passe doit contenir au moins une majuscule."
if not re.search(r"[a-z]", password): return "Le mot de passe doit contenir au moins une minuscule."
if not re.search(r"[0-9]", password): return "Le mot de passe doit contenir au moins un chiffre."
if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password): return "Le mot de passe doit contenir au moins un caractère spécial."
return None
def hash_password(password):
return hashlib.sha256(password.encode()).hexdigest()
def verify_password(stored_hash, provided_password):
return hmac.compare_digest(stored_hash, hash_password(provided_password))
def generate_totp_secret():
return pyotp.random_base32()
def verify_totp(secret, code):
totp = pyotp.TOTP(secret)
return totp.verify(code)
def send_invite_email(to_email, setup_link):
smtp_server = os.getenv("SMTP_SERVER")
smtp_port = int(os.getenv("SMTP_PORT", 587))
smtp_user = os.getenv("SMTP_USER")
smtp_pass = os.getenv("SMTP_PASS")
if not all([smtp_server, smtp_user, smtp_pass]):
return False
msg = MIMEMultipart()
msg['From'] = f"AETHAS38 <{smtp_user}>"
msg['To'] = to_email
msg['Subject'] = "Invitation au Hub AETHAS38"
body = f"Vous avez été invité(e) à rejoindre le hub IA AETHAS38.\nLien de configuration 2FA : {setup_link}"
msg.attach(MIMEText(body, 'plain', 'utf-8'))
try:
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
server.quit()
return True
except Exception:
return False
+57
View File
@@ -0,0 +1,57 @@
# catalog_manager.py
import os
import json
import re
import requests
CATALOG_FILE = "sessions/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:
MODELS_DATA = [
{"id": "groq|llama-3.1-8b-instant", "name": "Groq - Llama 3.1 (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)[cite: 11]
def get_budget():
mgmt_key = os.getenv("OPENROUTER_MANAGEMENT_KEY")
if not mgmt_key: return "Clé manquante[cite: 11]"
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} $"[cite: 11]
except: return "Erreur réseau[cite: 11]"
def parse_txt_catalog(content: str):
lines = content.split('\n')
models = []
current = {}
ext_date, ext_time = "Inconnue", ""
for line in lines:
line_trim = line.strip()
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, ext_time = match.group(1), 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[cite: 11]
+36
View File
@@ -0,0 +1,36 @@
# history_manager.py
import os
import json
import uuid
from datetime import datetime
HISTORY_FILE = "sessions/history.json"
def load_index():
if not os.path.exists(HISTORY_FILE): return {}
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
try: return json.load(f)
except: return {}[cite: 11]
def save_index(idx):
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump(idx, f, indent=4, ensure_ascii=False)[cite: 11]
def load_session_data(session_id):
path = f"sessions/{session_id}.json"
if not os.path.exists(path): return []
with open(path, "r", encoding="utf-8") as f:
try: return json.load(f)
except: return [][cite: 11]
def save_session_data(session_id, messages):
with open(f"sessions/{session_id}.json", "w", encoding="utf-8") as f:
json.dump(messages, f, indent=4, ensure_ascii=False)[cite: 11]
def create_new_session():
session_id = str(uuid.uuid4())
idx = load_index()
idx[session_id] = {"title": f"Projet du {datetime.now().strftime('%d/%m %H:%M')}", "pinned": False}
save_index(idx)
save_session_data(session_id, [])
return session_id[cite: 11]
+194 -127
View File
@@ -1,140 +1,207 @@
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, Response
from pydantic import BaseModel
import requests
import asyncio
# main.py
from fasthtml.common import *
import os
import uuid
import json
import subprocess
import urllib3
import socket
import config
import time
import requests
from dotenv import load_dotenv
# Forçage IPv4
old_getaddrinfo = socket.getaddrinfo
def force_ipv4_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
return old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)
socket.getaddrinfo = force_ipv4_getaddrinfo
from auth import verify_password, verify_totp
from catalog_manager import MODELS_DATA, load_catalog, save_catalog, get_budget, parse_txt_catalog
from history_manager import load_index, save_index, load_session_data, save_session_data, create_new_session
from ai_engine import ask_llm, async_ask_llm, log_event
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
load_dotenv()
app = FastAPI(title="AETHAS38 API")
theme_hdrs = [
Script(src="https://cdn.tailwindcss.com"),
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"),
Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/tokyo-night-dark.min.css"),
Style("""
body { background-color: #0f172a; color: #f8fafc; font-family: system-ui, sans-serif; margin: 0;}
.sidebar { background-color: #1e293b; border-right: 1px solid #333; height: 100vh; padding: 20px; display: flex; flex-direction: column;}
.main-content { padding: 20px; height: 100vh; display: flex; flex-direction: column; position: relative;}
.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-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; justify-content:center;}
.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; }
.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;}
.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; border: 1px solid #333;}
.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-request .htmx-indicator { display: block; }
.export-btn { display: block; background: #0f172a; border: 1px solid #00e5ff; color: #00e5ff; font-weight: bold; padding: 10px; border-radius: 5px; text-align: center; cursor: pointer; width: 100%;}
"""),
Script("""
function filterModels() {
let filter = document.querySelector('input[name="filter_type"]:checked').value;
document.querySelectorAll('.model-selector option').forEach(opt => {
if(opt.value === "") return;
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<=2; i++) { if(document.getElementById('worker'+i).value !== "") wCount++; }
if(wCount === 0) { alert("Erreur : Sélectionnez au moins une IA de travail."); return false; }
return true;
}
function updateFileCount(input) {
let label = document.getElementById('file-label-text');
if(input.files && input.files.length > 0) { label.innerText = `📎 ${input.files.length} fichier(s)`; label.style.color = '#00e5ff'; }
}
""")
]
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FRONTEND_DIR = os.path.join(BASE_DIR, "frontend")
SESSIONS_DIR = os.path.join(BASE_DIR, "sessions")
MEDIA_DIR = os.path.join(SESSIONS_DIR, "media")
TEMP_DIR = os.path.join(SESSIONS_DIR, "temp")
app, rt = fast_app(secret_key=os.getenv("SESSION_SECRET", "super-secret-key-fallback"), hdrs=theme_hdrs)
os.makedirs(MEDIA_DIR, exist_ok=True)
os.makedirs(TEMP_DIR, exist_ok=True)
os.makedirs("sessions", exist_ok=True)
app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static")
app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")
@rt('/login')
def get():
return Title("Connexion - AETHAS38"), Body(
Div(
H2("AETHAS 38 - Accès Sécurisé", style="color:#00e5ff; margin-bottom:20px; text-align:center;"),
Form(
Input(type="email", name="email", placeholder="E-mail", cls="input-box mb-3 w-full", required=True),
Input(type="password", name="password", placeholder="Mot de passe", cls="input-box mb-3 w-full", required=True),
Input(type="text", name="totp_code", placeholder="Code 2FA (6 chiffres)", cls="input-box mb-4 w-full", required=True),
Button("Se connecter", type="submit", cls="send-btn w-full"),
hx_post="/auth/login", hx_target="#auth-response"
),
Div(id="auth-response", style="color:red; margin-top:10px; text-align:center;"),
cls="sidebar", style="margin: auto; width: 400px; height: auto; border-radius: 10px; margin-top: 15vh;"
), style="background-color: #0f172a; height: 100vh; display: flex;"
)
class ExportRequest(BaseModel):
content: str
format: str # pdf, txt, md, json
type: str # prompt ou response
@rt('/auth/login')
def post(email: str, password: str, totp_code: str, session):
if email == os.getenv("ADMIN_EMAIL") and verify_password(os.getenv("ADMIN_PASSWORD_HASH"), password) and verify_totp(os.getenv("ADMIN_2FA_SECRET"), totp_code):
session["authenticated"] = True
session["role"] = "admin"
return Script("window.location.href = '/';")
return Div("Identifiants ou code 2FA incorrects.", style="color:#ef4444; font-weight:bold;")
@app.get("/", response_class=HTMLResponse)
async def serve_admin():
index_path = os.path.join(FRONTEND_DIR, "index.html")
with open(index_path, "r", encoding="utf-8") as f:
return f.read()
@rt('/logout')
def get(session):
session.clear()
return RedirectResponse('/login')
@app.get("/api/config")
async def get_config():
return config.load_config()
@rt('/check_updates')
def get(session):
if not session.get("authenticated"): return Span("")
try:
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:
return Span(f"🟢 Gitea Connecté ({res.json()[0].get('sha', '')[:7]})", style="color:#10b981;")
except: pass
return Span("🟠 Gitea hors ligne", style="color:#fbbf24;")
@app.post("/api/config")
async def update_config(data: dict):
config.save_config(data)
return {"status": "success", "message": "Configuration sauvegardée avec succès."}
def render_history_list():
idx = load_index()
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 idx.items():
items.append(Div(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"), cls="flex justify-between items-center w-full mb-2 bg-[#2a2a35] p-2 rounded border border-[#444]"))
return Div(*items, id="history-list-container", hx_swap_oob="true")
@app.post("/api/export")
async def export_content(req: ExportRequest):
filename_base = f"aethas38_{req.type}_{uuid.uuid4().hex[:8]}"
def render_model_dropdown(field_id, placeholder):
groups = {}
for m in MODELS_DATA:
prov = m.get('provider', 'Général')
if prov not in groups: groups[prov] = []
groups[prov].append(m)
opts = [Option("--- Laisser vide ---", value="")]
for prov in sorted(groups.keys()):
grp_opts = [Option(f"{m['name']} ({'Gratuit' if str(m.get('is_free'))=='True' else 'Payant'})", value=m['id'], **{"data-free": str(m.get('is_free', False)).lower()}) for m in groups[prov]]
opts.append(Optgroup(label=prov)(*grp_opts))
return Div(P(placeholder, style="color:#94a3b8; font-size:10px; font-weight:bold; margin-bottom:2px;"), Select(*opts, name=field_id, id=field_id, cls="model-selector", onchange="filterModels()"), style="margin-bottom: 8px;")
SESSION_PROGRESS = {}
@rt('/status/{sid}')
def get_status(sid: str):
return SESSION_PROGRESS.get(sid, "⚙️ Traitement en cours...")
@rt('/')
def get(session):
if not session.get("authenticated"): return RedirectResponse('/login')
session_id = create_new_session()
return Title("AETHAS38 Orchestrateur"), Body(
Div(
Div(
H2("AETHAS 38", style="color:#00e5ff; font-weight:bold; font-size:24px;"),
Div("⏳ Vérification GitHub...", hx_get="/check_updates", hx_trigger="load", style="font-size:10px; margin-bottom:15px;"),
Div(H3("Historique", style="color:#94a3b8; font-size:12px; font-weight:bold; margin-bottom:10px;"), render_history_list(), style="flex-grow:1; overflow:hidden;"),
Button("📥 Exporter Modèles", type="button", cls="export-btn mb-2", onclick="window.location.href='/export_models'"),
A("Déconnexion", href="/logout", cls="export-btn text-center", style="color:#ef4444; border-color:#ef4444;"),
Div(P("BUDGET", style="color:#94a3b8; font-size:10px; font-weight:bold; margin-top:8px;"), P(get_budget(), id="budget-display", style="color:#10b981; font-size:16px; font-weight:bold;")),
cls="sidebar"
),
Div(
Div(id="chat-history", cls="chat-container"),
Div(Span("⚙️ Préparation...", id="loading-text"), id="loading-tracker", cls="htmx-indicator"),
Form(
Input(type="hidden", name="session_id", value=session_id, id="current-session-id"),
Div(
Div(render_model_dropdown("writer", "1. Rédacteur"), render_model_dropdown("synthesizer", "7. Concaténeur"), style="border-right: 1px solid #444; padding-right:15px;"),
Div(render_model_dropdown("worker1", "2. Travailleur 1"), render_model_dropdown("worker2", "3. Travailleur 2"), style="padding: 0 15px;"),
cls="grid grid-cols-2 gap-4 mb-4 bg-[#151e2e] p-4 rounded border border-[#333]"
),
Div(
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 w-40"),
Input(type="text", name="msg", placeholder="Votre consigne...", cls="input-box", required=True),
Button("Lancer", type="submit", cls="send-btn"),
cls="input-row"
),
hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#loading-tracker", enctype="multipart/form-data",
onsubmit="return validateMultiIA();"
),
cls="main-content"
),
style="display: grid; grid-template-columns: 280px 1fr;"
)
)
@rt('/chat')
async def post(msg: str, session_id: str, session, writer: str = "", worker1: str = "", worker2: str = "", synthesizer: str = "", fichiers: list[UploadFile] = None):
if not session.get("authenticated"): return RedirectResponse('/login')
workers = [w for w in [worker1, worker2] if w]
if req.format == "txt":
return Response(
content=req.content,
media_type="text/plain",
headers={"Content-Disposition": f"attachment; filename={filename_base}.txt"}
)
elif req.format == "md":
return Response(
content=f"# AETHAS38 Export ({req.type.upper()})\n\n{req.content}",
media_type="text/markdown",
headers={"Content-Disposition": f"attachment; filename={filename_base}.md"}
)
elif req.format == "json":
json_data = json.dumps({"type": req.type, "content": req.content}, indent=4, ensure_ascii=False)
return Response(
content=json_data,
media_type="application/json",
headers={"Content-Disposition": f"attachment; filename={filename_base}.json"}
)
elif req.format == "pdf":
# Utilisation de LibreOffice en mode headless
html_filename = os.path.join(TEMP_DIR, f"{filename_base}.html")
pdf_filename = os.path.join(TEMP_DIR, f"{filename_base}.pdf")
# Création d'un HTML propre pour la conversion
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; margin: 40px; color: #333; }}
h1 {{ color: #00e5ff; border-bottom: 2px solid #ddd; padding-bottom: 10px; }}
pre, p {{ white-space: pre-wrap; line-height: 1.6; background: #f4f4f4; padding: 15px; border-radius: 5px; }}
</style>
</head>
<body>
<h1>AETHAS38 - Export {req.type.upper()}</h1>
<p>{req.content}</p>
</body>
</html>
"""
with open(html_filename, "w", encoding="utf-8") as f:
f.write(html_content)
try:
# Commande LibreOffice headless pour convertir le HTML en PDF
# Note: selon l'OS (Windows vs Linux Docker), la commande peut s'appeler 'soffice' ou 'libreoffice'
cmd = ["soffice", "--headless", "--convert-to", "pdf", "--outdir", TEMP_DIR, html_filename]
# Exécution synchrone dans un thread pour ne pas bloquer l'API
def run_libreoffice():
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
await asyncio.to_thread(run_libreoffice)
if not os.path.exists(pdf_filename):
raise Exception("La génération du PDF par LibreOffice a échoué.")
with open(pdf_filename, "rb") as pdf_file:
pdf_bytes = pdf_file.read()
# Nettoyage des fichiers temporaires
os.remove(html_filename)
os.remove(pdf_filename)
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename_base}.pdf"}
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erreur de génération PDF via LibreOffice : {str(e)}")
raise HTTPException(status_code=400, detail="Format d'export non supporté.")
SESSION_PROGRESS[session_id] = "⚡ Traitement des IA en cours..."
tasks = [async_ask_llm(session_id, w, msg) for w in workers]
results = await asyncio.gather(*tasks)
final_response = results[0] if results else "Aucun travailleur configuré."
session_data = load_session_data(session_id)
session_data.append({"user": msg, "ia": final_response})
save_session_data(session_id, session_data)
return Div(P("Vous", cls="msg-user"), Div(msg, cls="bubble-user"), P("AETHAS38", cls="msg-ia"), Div(final_response, cls="bubble-ia")), P(get_budget(), id="budget-display", style="color:#10b981; font-size:16px; font-weight:bold;", hx_swap_oob="true")
@rt('/export_models')
def get(session):
if not session.get("authenticated"): return RedirectResponse('/login')
return Response("=== CATALOGUE ===\nID: groq|llama-3.1-8b-instant\nTarif: Gratuit", media_type="text/plain", headers={"Content-Disposition": "attachment; filename=modeles.txt"})
@rt('/session/new')
def get(session):
if not session.get("authenticated"): return RedirectResponse('/login')
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')
messages = load_session_data(sid)
return tuple(Div(P("Vous", cls="msg-user"), Div(m['user'], cls="bubble-user"), P("AETHAS38", cls="msg-ia"), Div(m['ia'], cls="bubble-ia")) for m in messages) + (Input(type="hidden", name="session_id", value=sid, id="current-session-id", hx_swap_oob="true"),)
if __name__ == '__main__':
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=5001)
+148
View File
@@ -0,0 +1,148 @@
import os
import platform
import re
import pyotp
import qrcode
import psutil
from dotenv import load_dotenv
def clear_screen():
os.system('cls' if platform.system() == 'Windows' else 'clear')
def check_hardware():
print("=" * 60)
print(" 🖥️ ANALYSE DU MATÉRIEL ET LOGICIEL DU SERVEUR")
print("=" * 60)
cpu_count = psutil.cpu_count(logical=True)
ram_gb = round(psutil.virtual_memory().total / (1024**3), 2)
os_info = f"{platform.system()} {platform.release()}"
print(f"• Système d'exploitation : {os_info}")
print(f"• Cœurs CPU logiques : {cpu_count}")
print(f"• Mémoire RAM totale : {ram_gb} Go")
# Évaluation pour phuzzy/minecraft
print("\n[Vérification du modèle lourd : phuzzy/minecraft]")
if ram_gb < 16:
print("⚠️ AVERTISSEMENT CRITIQUE : Votre serveur dispose de moins de 16 Go de RAM/VRAM.")
print(" Le modèle 'phuzzy/minecraft' requiert des ressources importantes.")
print(" Risque de ralentissements sévères ou de plantage par manque de mémoire (OOM).")
else:
print("✅ La configuration matérielle semble suffisante pour exécuter les modèles locaux (Qwen & Phuzzy).")
print("-" * 60 + "\n")
def validate_password(password):
if len(password) < 12:
return "Le mot de passe doit contenir au moins 12 caractères."
if not re.search(r"[A-Z]", password):
return "Le mot de passe doit contenir au moins une majuscule."
if not re.search(r"[a-z]", password):
return "Le mot de passe doit contenir au moins une minuscule."
if not re.search(r"[0-9]", password):
return "Le mot de passe doit contenir au moins un chiffre."
if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password):
return "Le mot de passe doit contenir au moins un caractère spécial."
return None
def configure_admin():
print("=" * 60)
print(" 🔑 CRÉATION DU COMPTE ADMINISTRATEUR & SÉCURITÉ 2FA")
print("=" * 60)
email = input("Entrez l'e-mail administrateur : ").strip()
while True:
password = input("Définissez le mot de passe (min. 12 caractères, majuscules, minuscules, chiffres, symboles) : ").strip()
err = validate_password(password)
if err:
print(f"❌ Erreur : {err}")
else:
print("✅ Mot de passe valide.")
break
# Génération 2FA (TOTP)
totp_secret = pyotp.random_base32()
totp_uri = pyotp.totp.TOTP(totp_secret).provisioning_uri(name=email, issuer_name="AETHAS38-Hub")
print("\n--- DOUBLE AUTHENTIFICATION (2FA) ---")
print("Scannez le QR code ci-dessous avec votre application (Google Authenticator, KeePassXC, etc.) :")
# Génération d'un QR code dans le terminal en texte ASCII
qr = qrcode.QRCode()
qr.add_data(totp_uri)
qr.make(fit=True)
qr.print_ascii(invert=True)
print(nis := f"Clé secrète manuelle (si besoin) : {totp_secret}")
print("-" * 60 + "\n")
return email, password, totp_secret
def configure_api_keys():
print("=" * 60)
print(" 🌐 CONFIGURATION DES CLÉS API (PAR SPÉCIALITÉ)")
print("=" * 60)
print("Laissez vide si vous ne possédez pas la clé pour l'instant.\n")
keys = {}
print("1. [CODAGE & RAISONNEMENT] - OpenRouter & Management Key")
print(" -> Où la trouver : https://openrouter.ai/settings/keys")
keys["OPENROUTER_API_KEY"] = input(" Entrez OPENROUTER_API_KEY : ").strip()
keys["OPENROUTER_MANAGEMENT_KEY"] = input(" Entrez OPENROUTER_MANAGEMENT_KEY (pour le suivi du budget) : ").strip()
print("\n2. [CODAGE ULTRA-RAPIDE] - Groq")
print(" -> Où la trouver : https://console.groq.com/keys")
keys["GROQ_API_KEY"] = input(" Entrez GROQ_API_KEY : ").strip()
print("\n3. [MULTIMODAL & CODE] - Google Gemini")
print(" -> Où la trouver : https://aistudio.google.com/app/apikey")
keys["GEMINI_API_KEY"] = input(" Entrez GEMINI_API_KEY : ").strip()
print("\n4. [CODAGE AVANCÉ] - DeepSeek")
print(" -> Où la trouver : https://platform.deepseek.com/api_keys")
keys["DEEPSEEK_API_KEY"] = input(" Entrez DEEPSEEK_API_KEY : ").strip()
print("\n5. [TEXTE & GÉNÉRAL] - Mistral AI")
print(" -> Où la trouver : https://console.mistral.ai/api-keys/")
keys["MISTRAL_API_KEY"] = input(" Entrez MISTRAL_API_KEY : ").strip()
print("\n6. [INFRASTRUCTURE & IA EDGE] - Cloudflare")
print(" -> Où la trouver : Tableau de bord Cloudflare (Workers & Pages > AI)")
keys["CLOUDFLARE_ACCOUNT_ID"] = input(" Entrez CLOUDFLARE_ACCOUNT_ID : ").strip()
keys["CLOUDFLARE_API_TOKEN"] = input(" Entrez CLOUDFLARE_API_TOKEN : ").strip()
print("\n7. [MODÈLES LIBRES] - HuggingFace")
print(" -> Où la trouver : https://huggingface.co/settings/tokens")
keys["HUGGINGFACE_API_KEY"] = input(" Entrez HUGGINGFACE_API_KEY : ").strip()
print("-" * 60 + "\n")
return keys
def save_env(keys, admin_data):
env_content = f"""# ==========================================
# AETHAS38 - CONFIGURATION AUTOMATISÉE
# ==========================================
# Administrateur principal
ADMIN_EMAIL={admin_data[0]}
ADMIN_PASSWORD_HASH={admin_data[1]}
ADMIN_2FA_SECRET={admin_data[2]}
# Clés API Distantes
"""
for k, v in keys.items():
env_content += f"{k}={v}\n"
with open(".env", "w", encoding="utf-8") as f:
f.write(env_content)
print("✅ Fichier .env généré avec succès !")
if __name__ == "__main__":
clear_screen()
check_hardware()
admin_data = configure_admin()
keys = configure_api_keys()
save_env(keys, admin_data)
print("\n🎉 Installation et configuration initiale terminées avec succès !")
print("Vous pouvez maintenant lancer le service Dockge / Docker.")