On recommence
This commit is contained in:
1 parent
be2c860218
commit
2a5b7765a2
15 files changed
-1559
No files matched your search
@@ -1,26 +0,0 @@
|
|||||||
name: Build and Push Docker Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Log in to Gitea Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: gitea.aethas38.duckdns.org
|
|
||||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
|
||||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: gitea.aethas38.duckdns.org/xavier/multi-ia-codage:latest
|
|
||||||
-54
@@ -1,54 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
venv/
|
|
||||||
.env
|
|
||||||
.sesskey
|
|
||||||
aethas_history.json
|
|
||||||
Echange GEMINY.txt
|
|
||||||
.vscode/
|
|
||||||
aethas_logs.txt
|
|
||||||
Important/
|
|
||||||
ARCHIVAGE ECHANGE GEMINY.txt
|
|
||||||
test financier/
|
|
||||||
SAVE/
|
|
||||||
Fichiers de configuration sensibles
|
|
||||||
.env
|
|
||||||
*.env
|
|
||||||
.env.local
|
|
||||||
Environnements virtuels Python
|
|
||||||
venv/
|
|
||||||
.venv/
|
|
||||||
env/
|
|
||||||
ENV/
|
|
||||||
Fichiers compilés Python
|
|
||||||
pycache/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
Fichiers de logs
|
|
||||||
*.log
|
|
||||||
Dossiers d'IDE
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
Historique local de l'orchestrateur (si vous sauvegardez les conversations en JSON)
|
|
||||||
historique_*.json
|
|
||||||
sessions/
|
|
||||||
Projet IA.pdf
|
|
||||||
Multi IA Personnel & familliale.pdf
|
|
||||||
WF ATTACHEMENT STIC.pdf
|
|
||||||
gitea cmd.md
|
|
||||||
consignes.txt
|
|
||||||
history.json
|
|
||||||
# Environnement et clés secrètes
|
|
||||||
.env
|
|
||||||
|
|
||||||
# Dossiers dynamiques d'exécution
|
|
||||||
/logs/
|
|
||||||
/sessions/
|
|
||||||
|
|
||||||
# Fichiers de données locales et clés de session
|
|
||||||
history.json
|
|
||||||
*.sesskey
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
app_config.json
|
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
FROM python:3.13-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Installation des dépendances système (LibreOffice, etc.)
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
libreoffice \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copie et installation des dépendances Python
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# Copie de tout le code source de la racine
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
EXPOSE 5001
|
|
||||||
|
|
||||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5001"]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# 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]
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
# 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]
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Détecte le dossier parent (racine du projet) dynamiquement
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
SESSIONS_DIR = os.path.join(BASE_DIR, "sessions")
|
|
||||||
CONFIG_FILE = os.path.join(SESSIONS_DIR, "app_config.json")
|
|
||||||
|
|
||||||
def load_config():
|
|
||||||
os.makedirs(SESSIONS_DIR, exist_ok=True)
|
|
||||||
if not os.path.exists(CONFIG_FILE):
|
|
||||||
default_config = {
|
|
||||||
"api_keys": {"openrouter": "", "groq": "", "gemini": ""},
|
|
||||||
"smtp": {"host": "", "port": 587, "user": "", "password": ""},
|
|
||||||
"custom_providers": []
|
|
||||||
}
|
|
||||||
save_config(default_config)
|
|
||||||
return default_config
|
|
||||||
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
def save_config(config_data):
|
|
||||||
os.makedirs(SESSIONS_DIR, exist_ok=True)
|
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(config_data, f, indent=4, ensure_ascii=False)
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
services:
|
|
||||||
aethas38-core:
|
|
||||||
build: .
|
|
||||||
container_name: aethas38-multi-ia
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "5002:5001"
|
|
||||||
environment:
|
|
||||||
- TZ=Europe/Paris
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
extra_hosts:
|
|
||||||
- host.docker.internal:host-gateway
|
|
||||||
command: uvicorn main:app --host 0.0.0.0 --port 5001 --reload
|
|
||||||
volumes:
|
|
||||||
- /mnt/stockage_raid/AppData/MULTI-IA-AETHAS38/sessions:/app/sessions
|
|
||||||
- /mnt/stockage_raid/AppData/MULTI-IA-AETHAS38/logs:/app/logs
|
|
||||||
- /mnt/stockage_raid/AppData/MULTI-IA-AETHAS38/history.json:/app/history.json
|
|
||||||
networks:
|
|
||||||
- dockge-network
|
|
||||||
|
|
||||||
networks:
|
|
||||||
dockge-network:
|
|
||||||
external: true
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# 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]
|
|
||||||
-207
@@ -1,207 +0,0 @@
|
|||||||
# main.py
|
|
||||||
from fasthtml.common import *
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import requests
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
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'; }
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
|
|
||||||
app, rt = fast_app(secret_key=os.getenv("SESSION_SECRET", "super-secret-key-fallback"), hdrs=theme_hdrs)
|
|
||||||
|
|
||||||
os.makedirs("sessions", exist_ok=True)
|
|
||||||
|
|
||||||
@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;"
|
|
||||||
)
|
|
||||||
|
|
||||||
@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;")
|
|
||||||
|
|
||||||
@rt('/logout')
|
|
||||||
def get(session):
|
|
||||||
session.clear()
|
|
||||||
return RedirectResponse('/login')
|
|
||||||
|
|
||||||
@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;")
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
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)
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
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.")
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>AETHAS38 - Orchestrateur</title>
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
||||||
<style>
|
|
||||||
body { background-color: #0f172a; color: #f8fafc; }
|
|
||||||
.card { background-color: #1e293b; border: 1px solid #333; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
|
|
||||||
.input-box { width: 100%; padding: 10px; border-radius: 5px; background: #2a2a35; border: 1px solid #444; color: white; margin-top: 5px; outline: none; }
|
|
||||||
.input-box:focus { border-color: #00e5ff; }
|
|
||||||
.btn-primary { background-color: #00e5ff; color: #0f172a; font-weight: bold; padding: 10px 20px; border-radius: 5px; cursor: pointer; transition: 0.2s; }
|
|
||||||
.btn-primary:hover { background-color: #38bdf8; }
|
|
||||||
.btn-primary:disabled { background-color: #334155; color: #94a3b8; cursor: not-allowed; }
|
|
||||||
.btn-export { background-color: #334155; color: #38bdf8; font-size: 11px; font-weight: bold; padding: 4px 8px; border-radius: 4px; cursor: pointer; border: 1px solid #475569; }
|
|
||||||
.btn-export:hover { background-color: #475569; color: #00e5ff; }
|
|
||||||
.btn-danger { background-color: #f43f5e; color: white; padding: 5px 10px; border-radius: 5px; cursor: pointer; font-size: 12px; }
|
|
||||||
.tab-btn { padding: 10px 20px; font-weight: bold; cursor: pointer; transition: 0.2s; border-bottom: 2px solid transparent; }
|
|
||||||
.tab-active { color: #00e5ff; border-bottom-color: #00e5ff; }
|
|
||||||
.tab-inactive { color: #64748b; }
|
|
||||||
.tab-inactive:hover { color: #94a3b8; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="p-8">
|
|
||||||
<div id="app" class="max-w-5xl mx-auto">
|
|
||||||
|
|
||||||
<!-- En-tête & Suivi financier OpenRouter -->
|
|
||||||
<div class="flex justify-between items-center mb-6">
|
|
||||||
<h1 class="text-3xl font-bold text-[#00e5ff]">🤖 AETHAS38</h1>
|
|
||||||
<div class="bg-slate-800 border border-slate-700 px-4 py-2 rounded text-xs flex space-x-4">
|
|
||||||
<span>💰 OpenRouter - Crédit : <strong class="text-emerald-400">{{ orCredits.limit }}</strong></span>
|
|
||||||
<span>Consommation : <strong class="text-amber-400">{{ orCredits.usage }}</strong></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Navigation Onglets -->
|
|
||||||
<div class="flex space-x-2 mb-8 border-b border-gray-700">
|
|
||||||
<button @click="currentTab = 'admin'" :class="currentTab === 'admin' ? 'tab-active' : 'tab-inactive'" class="tab-btn">⚙️ Administration & Modèles</button>
|
|
||||||
<button @click="currentTab = 'studio'" :class="currentTab === 'studio' ? 'tab-active' : 'tab-inactive'" class="tab-btn">🎬 Studio Média</button>
|
|
||||||
<button @click="currentTab = 'documents'" :class="currentTab === 'documents' ? 'tab-active' : 'tab-inactive'" class="tab-btn">📂 Documents & Exports</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="notification" class="mb-4 p-4 rounded bg-emerald-900/50 border border-emerald-500 text-emerald-400">
|
|
||||||
{{ notification }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ONGLET : ADMINISTRATION & CATALOGUE DE MODÈLES -->
|
|
||||||
<div v-if="currentTab === 'admin'">
|
|
||||||
<div class="card">
|
|
||||||
<h2 class="text-xl font-bold text-purple-400 mb-4">🔑 Clés API & Suivi</h2>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="text-xs font-bold text-gray-400">OpenRouter API Key</label>
|
|
||||||
<input type="password" v-model="config.api_keys.openrouter" class="input-box" placeholder="sk-or-v1-...">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="text-xs font-bold text-gray-400">Groq API Key</label>
|
|
||||||
<input type="password" v-model="config.api_keys.groq" class="input-box" placeholder="gsk_...">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Gestion et Choix des Modèles (Gratuit / Payant & Catégorie) -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="flex justify-between items-center mb-4">
|
|
||||||
<h2 class="text-xl font-bold text-cyan-400">🎛️ Sélection & Filtrage des Modèles</h2>
|
|
||||||
<div class="space-x-2">
|
|
||||||
<select v-model="modelFilterPricing" class="bg-slate-800 text-xs text-gray-300 p-2 rounded border border-gray-700">
|
|
||||||
<option value="all">Tous les tarifs</option>
|
|
||||||
<option value="free">🟢 Gratuit uniquement</option>
|
|
||||||
<option value="paid">🟠 Payant</option>
|
|
||||||
</select>
|
|
||||||
<button @click="exportModelsJson" class="btn-export">Exporter Catalogue JSON</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
|
||||||
<div>
|
|
||||||
<label class="text-xs text-gray-400">Filtrer par Catégorie / Spécialité</label>
|
|
||||||
<input type="text" v-model="modelSearchCat" class="input-box text-xs" placeholder="Ex: Texte, Vision, Code...">
|
|
||||||
</div>
|
|
||||||
<div class="md:col-span-2">
|
|
||||||
<label class="text-xs text-gray-400">Modèle Actif Sélectionné</label>
|
|
||||||
<input type="text" v-model="selectedActiveModel" class="input-box text-xs text-emerald-400 font-bold" readonly>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-slate-900 p-4 rounded border border-gray-700 max-h-60 overflow-y-auto text-xs space-y-2">
|
|
||||||
<div v-for="m in filteredModels" :key="m.id" @click="selectedActiveModel = m.id" class="p-2 bg-slate-800 rounded hover:bg-slate-700 cursor-pointer flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<strong class="text-cyan-300">{{ m.name }}</strong> <span class="text-gray-500">({{ m.id }})</span>
|
|
||||||
<p class="text-gray-400">{{ m.description }}</p>
|
|
||||||
</div>
|
|
||||||
<span :class="m.pricing === 'free' ? 'text-emerald-400 bg-emerald-950 px-2 py-1 rounded text-[10px]' : 'text-amber-400 bg-amber-950 px-2 py-1 rounded text-[10px]'">
|
|
||||||
{{ m.pricing === 'free' ? 'GRATUIT' : 'PAYANT' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end mt-6">
|
|
||||||
<button @click="saveConfig" class="btn-primary text-lg">💾 Sauvegarder la Configuration</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ONGLET : STUDIO MÉDIA -->
|
|
||||||
<div v-if="currentTab === 'studio'">
|
|
||||||
<div class="card">
|
|
||||||
<h2 class="text-xl font-bold text-purple-400 mb-4">🎬 Studio Multimédia</h2>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
|
||||||
<div class="md:col-span-3">
|
|
||||||
<label class="text-xs font-bold text-gray-400">Votre Prompt</label>
|
|
||||||
<input type="text" v-model="mediaPrompt" class="input-box" placeholder="A futuristic city...">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="text-xs font-bold text-gray-400">Type</label>
|
|
||||||
<select v-model="mediaType" class="input-box">
|
|
||||||
<option value="image">🖼️ Image</option>
|
|
||||||
<option value="voice">🎙️ Voix</option>
|
|
||||||
<option value="music">🎵 Musique</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<button @click="generateMedia" :disabled="isGenerating || !mediaPrompt" class="btn-primary">
|
|
||||||
{{ isGenerating ? '⏳ Génération...' : '✨ Générer' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="generatedMediaUrl" class="card text-center">
|
|
||||||
<h3 class="font-bold text-gray-300 mb-4">Résultat :</h3>
|
|
||||||
<img :src="generatedMediaUrl" class="max-w-full h-auto mx-auto rounded border border-gray-600 shadow-lg">
|
|
||||||
<div class="mt-4"><a :href="generatedMediaUrl" download class="text-[#00e5ff] hover:underline text-sm font-bold">⬇️ Télécharger</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ONGLET : DOCUMENTS & EXPORTS -->
|
|
||||||
<div v-if="currentTab === 'documents'">
|
|
||||||
<div class="card">
|
|
||||||
<h2 class="text-xl font-bold text-blue-400 mb-4">📂 Analyse de Documents & Exports Multi-formats</h2>
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="text-xs font-bold text-gray-400">Sélectionner un fichier (PDF, Word, TXT)</label>
|
|
||||||
<input type="file" @change="handleFileUpload" class="input-box file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#00e5ff] file:text-[#0f172a] hover:file:bg-[#38bdf8] cursor-pointer">
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="text-xs font-bold text-gray-400">Instruction associée</label>
|
|
||||||
<textarea v-model="docPrompt" rows="3" class="input-box" placeholder="Ex: Résume ce document..."></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<button @click="uploadDocument" :disabled="isUploading || !selectedFile" class="btn-primary">
|
|
||||||
{{ isUploading ? '⏳ Traitement...' : '🚀 Envoyer et Analyser' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="docResult" class="card">
|
|
||||||
<div class="flex justify-between items-center mb-2">
|
|
||||||
<h3 class="font-bold text-emerald-400">Rapport : {{ docResult.filename }}</h3>
|
|
||||||
<!-- Boutons d'export directs (PDF, TXT, MD, JSON) -->
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<span class="text-xs text-gray-400 self-center">Exporter en :</span>
|
|
||||||
<button @click="exportData(docResult.full_text, 'document_result', 'pdf')" class="btn-export">PDF</button>
|
|
||||||
<button @click="exportData(docResult.full_text, 'document_result', 'txt')" class="btn-export">TXT</button>
|
|
||||||
<button @click="exportData(docResult.full_text, 'document_result', 'md')" class="btn-export">MD</button>
|
|
||||||
<button @click="exportData(docResult.full_text, 'document_result', 'json')" class="btn-export">JSON</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-slate-900 p-4 rounded border border-gray-700 whitespace-pre-wrap text-sm text-gray-200">
|
|
||||||
{{ docResult.extracted_preview }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const { createApp, ref, computed, onMounted } = Vue;
|
|
||||||
createApp({
|
|
||||||
setup() {
|
|
||||||
const currentTab = ref('admin');
|
|
||||||
const config = ref({ api_keys: {}, custom_providers: [] });
|
|
||||||
const notification = ref("");
|
|
||||||
const orCredits = ref({ limit: "Chargement...", usage: "..." });
|
|
||||||
|
|
||||||
// Modèles et filtres
|
|
||||||
const modelFilterPricing = ref("free");
|
|
||||||
const modelSearchCat = ref("");
|
|
||||||
const selectedActiveModel = ref("groq/llama-3.1-8b-instant");
|
|
||||||
const availableModels = ref([
|
|
||||||
{ id: "groq/llama-3.1-8b-instant", name: "Groq Llama 3.1 8B", pricing: "free", category: "Texte", description: "Modèle ultra-rapide sur Groq" },
|
|
||||||
{ id: "google/gemini-1.5-flash", name: "Gemini 1.5 Flash", pricing: "free", category: "Vision / Multimodal", description: "Modèle léger Google" },
|
|
||||||
{ id: "anthropic/claude-3.5-sonnet", name: "Claude 3.5 Sonnet", pricing: "paid", category: "Texte / Code", description: "Modèle de pointe Anthropic" },
|
|
||||||
{ id: "black-forest-labs/flux-schnell", name: "Flux Schnell", pricing: "free", category: "Image", description: "Génération d'images rapide" }
|
|
||||||
]);
|
|
||||||
|
|
||||||
const filteredModels = computed(() => {
|
|
||||||
return availableModels.value.filter(m => {
|
|
||||||
const matchPricing = modelFilterPricing.value === 'all' || m.pricing === modelFilterPricing.value;
|
|
||||||
const matchCat = !modelSearchCat.value || m.category.toLowerCase().includes(modelSearchCat.value.toLowerCase()) || m.name.toLowerCase().includes(modelSearchCat.value.toLowerCase());
|
|
||||||
return matchPricing && matchCat;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const mediaPrompt = ref("");
|
|
||||||
const mediaType = ref("image");
|
|
||||||
const isGenerating = ref(false);
|
|
||||||
const generatedMediaUrl = ref(null);
|
|
||||||
|
|
||||||
const selectedFile = ref(null);
|
|
||||||
const docPrompt = ref("");
|
|
||||||
const isUploading = ref(false);
|
|
||||||
const docResult = ref(null);
|
|
||||||
|
|
||||||
const loadConfig = async () => {
|
|
||||||
const res = await fetch('/api/config');
|
|
||||||
config.value = await res.json();
|
|
||||||
|
|
||||||
const credRes = await fetch('/api/openrouter/credits');
|
|
||||||
orCredits.value = await credRes.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveConfig = async () => {
|
|
||||||
const res = await fetch('/api/config', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(config.value)
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
notification.value = data.message;
|
|
||||||
setTimeout(() => notification.value = "", 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportModelsJson = () => {
|
|
||||||
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(availableModels.value, null, 4));
|
|
||||||
const downloadAnchor = document.createElement('a');
|
|
||||||
downloadAnchor.setAttribute("href", dataStr);
|
|
||||||
downloadAnchor.setAttribute("download", "aethas38_models_catalog.json");
|
|
||||||
document.body.appendChild(downloadAnchor);
|
|
||||||
downloadAnchor.click();
|
|
||||||
downloadAnchor.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateMedia = async () => {
|
|
||||||
if (!mediaPrompt.value) return;
|
|
||||||
isGenerating.value = true;
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/media/generate', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ prompt: mediaPrompt.value, media_type: mediaType.value })
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (res.ok) generatedMediaUrl.value = data.url;
|
|
||||||
else alert(data.detail);
|
|
||||||
} catch (e) { alert("Erreur réseau"); }
|
|
||||||
finally { isGenerating.value = false; }
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileUpload = (event) => { selectedFile.value = event.target.files[0]; };
|
|
||||||
|
|
||||||
const uploadDocument = async () => {
|
|
||||||
if (!selectedFile.value) return;
|
|
||||||
isUploading.value = true;
|
|
||||||
docResult.value = null;
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", selectedFile.value);
|
|
||||||
formData.append("prompt", docPrompt.value);
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/documents/process', { method: 'POST', body: formData });
|
|
||||||
const data = await res.json();
|
|
||||||
if (res.ok) docResult.value = data;
|
|
||||||
else alert(data.detail || "Erreur document");
|
|
||||||
} catch (e) { alert("Erreur connexion"); }
|
|
||||||
finally { isUploading.value = false; }
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportData = async (content, type, format) => {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/export', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ content, format, type })
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error("Erreur export");
|
|
||||||
const blob = await res.blob();
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `aethas38_${type}.${format}`;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
} catch (e) { alert("Erreur téléchargement export."); }
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(loadConfig);
|
|
||||||
return {
|
|
||||||
currentTab, config, notification, orCredits, saveConfig,
|
|
||||||
modelFilterPricing, modelSearchCat, selectedActiveModel, availableModels, filteredModels, exportModelsJson,
|
|
||||||
mediaPrompt, mediaType, isGenerating, generatedMediaUrl, generateMedia,
|
|
||||||
selectedFile, docPrompt, isUploading, docResult, handleFileUpload, uploadDocument, exportData
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}).mount('#app');
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"8f36f77c-688a-4608-bfcc-38f36090ddd4": {
|
|
||||||
"title": "Discussion du 05/10 10:47",
|
|
||||||
"pinned": false,
|
|
||||||
"messages": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-537
@@ -1,537 +0,0 @@
|
|||||||
from fasthtml.common import *
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import uuid
|
|
||||||
import asyncio
|
|
||||||
from datetime import datetime
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# 2. INITIALISATION DE FASTHTML EN PREMIER (C'est ici que 'rt' est créé)
|
|
||||||
app, rt = fast_app()
|
|
||||||
|
|
||||||
from auth import validate_password_strength, hash_password, verify_password, generate_totp_secret, verify_totp
|
|
||||||
|
|
||||||
# Exemple de structure pour les routes de connexion (Login / Setup initial)
|
|
||||||
@rt('/login')
|
|
||||||
def get():
|
|
||||||
return Title("Connexion - AETHAS38"), Body(
|
|
||||||
Div(
|
|
||||||
H2("AETHAS 38 - Sécurité", style="color:#00e5ff; margin-bottom:20px;"),
|
|
||||||
Form(
|
|
||||||
Input(type="email", name="email", placeholder="E-mail", cls="input-box mb-3", required=True),
|
|
||||||
Input(type="password", name="password", placeholder="Mot de passe", cls="input-box mb-3", required=True),
|
|
||||||
Input(type="text", name="totp_code", placeholder="Code 2FA (6 chiffres)", cls="input-box mb-3", 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;"),
|
|
||||||
cls="sidebar", style="margin: auto; width: 400px; height: auto; border-radius: 10px; margin-top: 15vh;"
|
|
||||||
), style="background-color: #0f172a; height: 100vh; display: flex;"
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- 0. INITIALISATION DES DOSSIERS (BDD & LOGS) ---
|
|
||||||
os.makedirs("sessions", exist_ok=True)
|
|
||||||
os.makedirs("logs", exist_ok=True)
|
|
||||||
|
|
||||||
# Un seul fichier de log par lancement d'application
|
|
||||||
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):
|
|
||||||
"""Enregistre chaque action dans le fichier log global avec catégorisation."""
|
|
||||||
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")
|
|
||||||
|
|
||||||
# --- 1. GESTION DE L'HISTORIQUE (INDEX & SESSIONS) ---
|
|
||||||
HISTORY_FILE = "history.json"
|
|
||||||
|
|
||||||
def load_index():
|
|
||||||
if not os.path.exists(HISTORY_FILE): return {}
|
|
||||||
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
|
|
||||||
try:
|
|
||||||
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 {}
|
|
||||||
|
|
||||||
def save_index(idx):
|
|
||||||
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(idx, f, indent=4, ensure_ascii=False)
|
|
||||||
|
|
||||||
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 []
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def create_new_session():
|
|
||||||
session_id = str(uuid.uuid4())
|
|
||||||
idx = load_index()
|
|
||||||
idx[session_id] = {"title": f"Discussion du {datetime.now().strftime('%d/%m %H:%M')}", "pinned": False}
|
|
||||||
save_index(idx)
|
|
||||||
save_session_data(session_id, [])
|
|
||||||
log_event(session_id, "USER_ACTION", "Nouvelle session initialisée.")
|
|
||||||
return session_id
|
|
||||||
|
|
||||||
def render_history_list():
|
|
||||||
idx = load_index()
|
|
||||||
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")
|
|
||||||
]
|
|
||||||
|
|
||||||
for sid, data in sorted_sessions:
|
|
||||||
pin_icon = "📍" if data.get('pinned') else "📌"
|
|
||||||
pin_color = "#10b981" if data.get('pinned') else "#94a3b8"
|
|
||||||
|
|
||||||
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(
|
|
||||||
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("🗑️", 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'}}); }}"),
|
|
||||||
cls="flex-shrink-0"
|
|
||||||
),
|
|
||||||
cls="flex justify-between items-center history-btn w-full mb-1"
|
|
||||||
)
|
|
||||||
items.append(item)
|
|
||||||
return Div(*items, id="history-list-container", hx_swap_oob="true")
|
|
||||||
|
|
||||||
|
|
||||||
# --- 2. GESTION DU CATALOGUE (GENERIQUE & OPENROUTER) ---
|
|
||||||
MODELS_DATA = []
|
|
||||||
def init_models():
|
|
||||||
global MODELS_DATA
|
|
||||||
MODELS_DATA = [
|
|
||||||
{"id": "groq|llama3-8b-8192", "name": "Groq - Llama 3 (8B)", "is_free": True, "description": "Modèle ultra-rapide hébergé par Groq. Idéal pour les tâches générales, la structuration de données et le code basique.", "architecture": {"modality": "text"}},
|
|
||||||
{"id": "gemini|gemini-1.5-flash", "name": "Google - Gemini 1.5 Flash", "is_free": True, "description": "Modèle multimodal très rapide et léger de Google. Parfait pour le raisonnement rapide, le code et l'analyse de longs contextes.", "architecture": {"modality": "text/vision"}},
|
|
||||||
{"id": "deepseek|deepseek-coder", "name": "DeepSeek - Coder", "is_free": False, "description": "Modèle d'élite spécialisé dans l'ingénierie logicielle, la création d'algorithmes et la génération de code complexe.", "architecture": {"modality": "text/code"}}
|
|
||||||
]
|
|
||||||
try:
|
|
||||||
res = requests.get("https://openrouter.ai/api/v1/models", timeout=5)
|
|
||||||
if res.status_code == 200:
|
|
||||||
for m in res.json().get("data", []):
|
|
||||||
m["id"] = f"openrouter|{m['id']}"
|
|
||||||
m["name"] = f"OR - {m['name']}"
|
|
||||||
MODELS_DATA.append(m)
|
|
||||||
except Exception as e:
|
|
||||||
log_event("system", "ERROR", f"Erreur chargement catalogue OpenRouter : {e}")
|
|
||||||
init_models()
|
|
||||||
|
|
||||||
def get_model_options(filter_type="all"):
|
|
||||||
options = []
|
|
||||||
for m in sorted(MODELS_DATA, key=lambda x: x['name']):
|
|
||||||
is_free = m.get("is_free", "free" in m.get("id", "").lower())
|
|
||||||
if filter_type == "free" and not is_free: continue
|
|
||||||
if filter_type == "paid" and is_free: continue
|
|
||||||
tag = "Gratuit" if is_free else "Payant"
|
|
||||||
options.append(Option(f"{m['name']} ({tag})", value=m['id']))
|
|
||||||
return Select(*options, name="model_id", cls="model-select", style="width: 100%;")
|
|
||||||
|
|
||||||
|
|
||||||
# --- 3. MOTEUR D'INTELLIGENCE & GESTION DES ERREURS ---
|
|
||||||
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 lecture"
|
|
||||||
|
|
||||||
def handle_api_error(session_id, provider, raw_error):
|
|
||||||
log_event(session_id, "ERROR", f"Erreur API {provider} : {raw_error}")
|
|
||||||
sys_prompt = "Tu es un assistant technique. Une API distante a renvoyé l'erreur suivante. Traduis-la en français simplement et propose une explication claire pour aider l'utilisateur."
|
|
||||||
try:
|
|
||||||
qwen_res = requests.post("http://localhost:11434/api/generate", json={"model": "qwen2.5-coder:7b", "prompt": f"{sys_prompt}\n\nErreur ({provider}): {raw_error}", "stream": False}, timeout=15).json()["response"]
|
|
||||||
return f"⚠️ **Alerte Serveur ({provider})**\n{qwen_res}"
|
|
||||||
except:
|
|
||||||
return f"⚠️ Erreur brute ({provider}) : {raw_error}"
|
|
||||||
|
|
||||||
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 API distantes : {provider} | Modèle : {actual_model}")
|
|
||||||
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 handle_api_error(session_id, provider, res.text)
|
|
||||||
return res.json()["choices"][0]["message"]["content"]
|
|
||||||
|
|
||||||
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 handle_api_error(session_id, provider, res.text)
|
|
||||||
return res.json()["choices"][0]["message"]["content"]
|
|
||||||
|
|
||||||
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 handle_api_error(session_id, provider, res.text)
|
|
||||||
return res.json()["candidates"][0]["content"]["parts"][0]["text"]
|
|
||||||
|
|
||||||
return handle_api_error(session_id, provider, "Fournisseur non implémenté.")
|
|
||||||
except Exception as e:
|
|
||||||
return handle_api_error(session_id, provider, str(e))
|
|
||||||
|
|
||||||
|
|
||||||
# --- 4. PIPELINES DE TRAITEMENT ---
|
|
||||||
def pipeline_minecraft(session_id, raw_msg):
|
|
||||||
log_event(session_id, "PIPELINE", "Lancement Pipeline 100% Local Minecraft (Qwen -> Phuzzy -> Qwen)")
|
|
||||||
try:
|
|
||||||
en_prompt = requests.post("http://localhost:11434/api/generate", json={"model": "qwen2.5-coder:7b", "prompt": f"Translate this Minecraft modding request into technical English. Return ONLY English text.\n\n{raw_msg}", "stream": False}).json()["response"]
|
|
||||||
phuzzy_res = requests.post("http://localhost:11434/api/generate", json={"model": "phuzzy:latest", "prompt": en_prompt, "stream": False}).json()["response"]
|
|
||||||
final_fr = requests.post("http://localhost:11434/api/generate", json={"model": "qwen2.5-coder:7b", "prompt": f"Traduis les explications techniques en français. NE TRADUIS PAS le code.\n\n{phuzzy_res}", "stream": False}).json()["response"]
|
|
||||||
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):
|
|
||||||
log_event(session_id, "PIPELINE", "Lancement Pipeline Consolidation Multi-IA")
|
|
||||||
try:
|
|
||||||
opt_prompt = requests.post("http://localhost:11434/api/generate", json={"model": "qwen2.5-coder:7b", "prompt": f"Optimise cette demande pour des LLM codeurs. Sois ultra précis.\n\n{raw_msg}", "stream": False}).json()["response"]
|
|
||||||
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 = requests.post("http://localhost:11434/api/generate", json={"model": "qwen2.5-coder:7b", "prompt": final_prompt, "stream": False}).json()["response"]
|
|
||||||
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"
|
|
||||||
|
|
||||||
|
|
||||||
# --- 5. INTERFACE UTILISATEUR & ROUTES ---
|
|
||||||
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;}
|
|
||||||
.model-select { padding: 12px; border-radius: 5px; background: #2a2a35; border: 1px solid #444; color: #00e5ff; outline: none; font-weight: bold; cursor: pointer;}
|
|
||||||
.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; }
|
|
||||||
.bubble-ia pre { background: #111827; padding: 15px; border-radius: 8px; margin-top: 10px; position: relative; border: 1px solid #333;}
|
|
||||||
.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: 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; }
|
|
||||||
.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; }
|
|
||||||
.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; transition: 0.2s; width: 100%;}
|
|
||||||
.export-btn:hover { background: #00e5ff; color: #0f172a; }
|
|
||||||
"""),
|
|
||||||
Script("""
|
|
||||||
function updateUI() {
|
|
||||||
let ctx = document.querySelector('input[name="context_type"]:checked').value;
|
|
||||||
let modBox = document.getElementById('model-selection-area');
|
|
||||||
if(ctx === 'minecraft' || ctx === 'consolidated') { modBox.style.display = 'none'; }
|
|
||||||
else { modBox.style.display = 'flex'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFileCount(input) {
|
|
||||||
let label = document.getElementById('file-label-text');
|
|
||||||
if(input && input.files && input.files.length > 0) {
|
|
||||||
label.innerText = `📎 ${input.files.length} fichier(s)`;
|
|
||||||
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) {
|
|
||||||
content.querySelectorAll('.bubble-ia:not(.rendered)').forEach(function(el) {
|
|
||||||
el.innerHTML = marked.parse(el.textContent);
|
|
||||||
el.classList.add('rendered');
|
|
||||||
el.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); });
|
|
||||||
let chat = document.getElementById('chat-history'); chat.scrollTop = chat.scrollHeight;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
|
|
||||||
app, rt = fast_app(hdrs=theme_hdrs)
|
|
||||||
|
|
||||||
@rt('/session/new')
|
|
||||||
def get():
|
|
||||||
sid = create_new_session()
|
|
||||||
new_hidden = Input(type="hidden", name="session_id", value=sid, id="current-session-id", hx_swap_oob="true")
|
|
||||||
return Div(), new_hidden, render_history_list()
|
|
||||||
|
|
||||||
@rt('/session/load/{sid}')
|
|
||||||
def get(sid: str):
|
|
||||||
log_event(sid, "USER_ACTION", "Chargement de la session.")
|
|
||||||
messages = load_session_data(sid)
|
|
||||||
bubbles = []
|
|
||||||
for msg in messages:
|
|
||||||
bubbles.append(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")
|
|
||||||
))
|
|
||||||
new_hidden = Input(type="hidden", name="session_id", value=sid, id="current-session-id", hx_swap_oob="true")
|
|
||||||
return tuple(bubbles) + (new_hidden,)
|
|
||||||
|
|
||||||
@rt('/history/pin/{sid}')
|
|
||||||
def post_pin(sid: str):
|
|
||||||
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):
|
|
||||||
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):
|
|
||||||
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 définitivement.")
|
|
||||||
return render_history_list()
|
|
||||||
|
|
||||||
@rt('/export_models')
|
|
||||||
def get(session):
|
|
||||||
if not session.get("authenticated"): return RedirectResponse('/login')
|
|
||||||
log_event("system", "SYSTEM", "Demande d'exportation du catalogue de modèles.")
|
|
||||||
now = datetime.now()
|
|
||||||
txt_content = f"=== CATALOGUE COMPLET DES MODÈLES (Généré le {now.strftime('%d/%m/%Y à %H:%M:%S')}) ===\n\n"
|
|
||||||
|
|
||||||
google_banned = False # Sécurité pour ne pas figer si Google bloque l'IP
|
|
||||||
|
|
||||||
for m in MODELS_DATA:
|
|
||||||
name = m.get('name', 'Inconnu')
|
|
||||||
m_id = m.get('id', 'Inconnu')
|
|
||||||
|
|
||||||
usage = "Général / Texte"
|
|
||||||
arch_mod = str(m.get('architecture', {}).get('modality', '')).lower()
|
|
||||||
m_id_lower = m_id.lower()
|
|
||||||
|
|
||||||
if "vision" in arch_mod or "vision" in m_id_lower or "vl" in m_id_lower or "image" in arch_mod:
|
|
||||||
usage = "Vision (Analyse d'images & Multimodal)"
|
|
||||||
elif "coder" in m_id_lower or "code" in m_id_lower or "math" in m_id_lower:
|
|
||||||
usage = "Codage & Mathématiques"
|
|
||||||
elif "video" in m_id_lower:
|
|
||||||
usage = "Vidéo"
|
|
||||||
elif "rp" in m_id_lower or "uncensored" in m_id_lower or "roleplay" in m_id_lower:
|
|
||||||
usage = "Non-censuré / Roleplay"
|
|
||||||
|
|
||||||
is_free = m.get("is_free")
|
|
||||||
if is_free is None:
|
|
||||||
pricing = m.get('pricing', {})
|
|
||||||
p_prompt = float(pricing.get('prompt', -1))
|
|
||||||
p_comp = float(pricing.get('completion', -1))
|
|
||||||
cout = "Gratuit" if p_prompt == 0.0 and p_comp == 0.0 else f"Payant (In: {p_prompt}, Out: {p_comp})"
|
|
||||||
else:
|
|
||||||
cout = "Gratuit" if is_free else "Payant"
|
|
||||||
|
|
||||||
desc_courte = str(m.get('description', '')).replace('\n', ' ')[:150]
|
|
||||||
desc_fr = desc_courte
|
|
||||||
|
|
||||||
if "openrouter" in m_id and desc_courte:
|
|
||||||
if not google_banned:
|
|
||||||
try:
|
|
||||||
url = "https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=fr&dt=t&q=" + requests.utils.quote(desc_courte)
|
|
||||||
trans_res = requests.get(url, timeout=1.5)
|
|
||||||
if trans_res.status_code == 200:
|
|
||||||
desc_fr = "".join([x[0] for x in trans_res.json()[0]])
|
|
||||||
else:
|
|
||||||
google_banned = True
|
|
||||||
desc_fr += " [Traduction interrompue: Sécurité anti-spam]"
|
|
||||||
except:
|
|
||||||
google_banned = True
|
|
||||||
desc_fr += " [Traduction interrompue: Délai d'attente]"
|
|
||||||
else:
|
|
||||||
desc_fr = desc_courte
|
|
||||||
|
|
||||||
if not desc_fr: desc_fr = "Aucune description fournie."
|
|
||||||
|
|
||||||
# SÉCURITÉ DOCKER : Découpage propre de la chaîne pour éviter le crash f-string
|
|
||||||
txt_content += (
|
|
||||||
f"Nom : {name}\n"
|
|
||||||
f"ID : {m_id}\n"
|
|
||||||
f"Tarif : {cout}\n"
|
|
||||||
f"Spécialité : {usage}\n"
|
|
||||||
f"Description : {desc_fr}\n"
|
|
||||||
f"{'-'*60}\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Forcer le téléchargement du fichier TXT
|
|
||||||
filename = f"modeles_ia_{datetime.now().strftime('%Y%m%d_%H%M')}.txt"
|
|
||||||
return Response(txt_content, media_type="text/plain", headers={"Content-Disposition": f"attachment; filename={filename}"})
|
|
||||||
|
|
||||||
@rt('/')
|
|
||||||
def get():
|
|
||||||
session_id = create_new_session()
|
|
||||||
return Title("AETHAS38 Multi-IA"), Body(
|
|
||||||
Div(
|
|
||||||
Div(
|
|
||||||
H2("AETHAS 38", style="color:#00e5ff; font-weight:bold; font-size:24px;"),
|
|
||||||
P("Tech Core - Multi IA", style="color:#a855f7; margin-bottom: 20px;"),
|
|
||||||
|
|
||||||
Div(
|
|
||||||
H3("Historique de Code", style="color:#94a3b8; font-size:12px; font-weight:bold; margin-bottom:10px;"),
|
|
||||||
render_history_list(),
|
|
||||||
style="display:flex; flex-direction:column; flex-grow:1; margin-bottom: 10px; overflow:hidden;"
|
|
||||||
),
|
|
||||||
|
|
||||||
# Le bouton fait désormais appel à la fonction JavaScript de téléchargement
|
|
||||||
Button("📥 Exporter Modèles (.txt)", type="button", cls="export-btn w-full mt-2", onclick="downloadModels()"),
|
|
||||||
|
|
||||||
Div(
|
|
||||||
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", style="margin-top:15px;"
|
|
||||||
),
|
|
||||||
cls="sidebar"
|
|
||||||
),
|
|
||||||
Div(
|
|
||||||
Div(id="chat-history", cls="chat-container"),
|
|
||||||
Div("⚙️ Traitement de l'Architecture IA en cours...", id="loading-tracker", cls="htmx-indicator"),
|
|
||||||
|
|
||||||
Form(
|
|
||||||
Input(type="hidden", name="session_id", value=session_id, id="current-session-id"),
|
|
||||||
Div(
|
|
||||||
Label(Input(type="radio", name="context_type", value="generic", checked=True, onchange="updateUI()"), " 💻 Code Générique (Modèle Unique)"),
|
|
||||||
Label(Input(type="radio", name="context_type", value="consolidated", onchange="updateUI()"), " 🧠 Consolidation (Multi-IA ➔ Qwen)"),
|
|
||||||
Label(Input(type="radio", name="context_type", value="minecraft", onchange="updateUI()"), " ⛏ Minecraft (100% Local)"),
|
|
||||||
cls="context-radio-group text-sm text-white flex gap-4"
|
|
||||||
),
|
|
||||||
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(get_model_options("all"), id="model-select-wrapper", style="flex-grow: 1;"),
|
|
||||||
cls="flex gap-2 mb-2 w-full", id="model-selection-area"
|
|
||||||
),
|
|
||||||
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"),
|
|
||||||
Input(type="text", name="msg", placeholder="Insérez votre requête...", cls="input-box", required=True),
|
|
||||||
Button("Envoyer", 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",
|
|
||||||
**{"hx-on:htmx:afterRequest": "if(event.detail.successful) { this.reset(); updateFileCount(document.getElementById('file-upload')); }"}
|
|
||||||
),
|
|
||||||
cls="main-content"
|
|
||||||
),
|
|
||||||
style="display: grid; grid-template-columns: 250px 1fr;"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@rt('/filter_models')
|
|
||||||
def get(filter_type: str): return get_model_options(filter_type)
|
|
||||||
|
|
||||||
@rt('/chat')
|
|
||||||
async def post(msg: str, model_id: str, session_id: str, context_type: str = "generic", fichiers: list[UploadFile] = None):
|
|
||||||
log_event(session_id, "USER_ACTION", f"Envoi d'un message (Contexte: {context_type})")
|
|
||||||
|
|
||||||
files_context = ""
|
|
||||||
noms_fichiers = []
|
|
||||||
if fichiers:
|
|
||||||
for f in fichiers:
|
|
||||||
if f.filename:
|
|
||||||
noms_fichiers.append(f.filename)
|
|
||||||
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"
|
|
||||||
log_event(session_id, "FILE_SYS", f"Fichier non lisible ignoré : {f.filename}")
|
|
||||||
if noms_fichiers: log_event(session_id, "FILE_SYS", f"Fichiers joints : {', '.join(noms_fichiers)}")
|
|
||||||
|
|
||||||
full_req = f"Fichiers fournis:\n{files_context}\nDemande: {msg}" if files_context else msg
|
|
||||||
|
|
||||||
if context_type == "minecraft":
|
|
||||||
ia_reponse, pipeline_info = pipeline_minecraft(session_id, full_req)
|
|
||||||
nom_affichage = "Local - Phuzzy/Minecraft"
|
|
||||||
|
|
||||||
elif context_type == "consolidated":
|
|
||||||
ia_reponse, pipeline_info = await pipeline_consolidated(session_id, full_req)
|
|
||||||
nom_affichage = "Qwen Synthèse (via Groq/Gemini)"
|
|
||||||
|
|
||||||
else:
|
|
||||||
opt_prompt = requests.post("http://localhost:11434/api/generate", json={"model": "qwen2.5-coder:7b", "prompt": f"Optimise cette demande pour un LLM codeur. Retourne UNIQUEMENT le prompt.\n\n{full_req}", "stream": False}).json()["response"]
|
|
||||||
parts = model_id.split("|", 1)
|
|
||||||
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) joint(s)</span>" if noms_fichiers else ""
|
|
||||||
info_local = f"<br><span style='color:#38bdf8; font-size:10px;'>⚙️ Pipeline : {pipeline_info}</span>"
|
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
save_session_data(session_id, session_data)
|
|
||||||
log_event(session_id, "SYSTEM", "Réponse sauvegardée avec succès.")
|
|
||||||
|
|
||||||
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")
|
|
||||||
)
|
|
||||||
|
|
||||||
updated_budget = P(get_budget(), id="budget-display", style="color:#10b981; font-size:18px; font-weight:bold;", hx_swap_oob="true")
|
|
||||||
return chat_bubble, updated_budget
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
serve(port=5001)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
python-fasthtml
|
|
||||||
requests
|
|
||||||
python-dotenv
|
|
||||||
pyotp
|
|
||||||
qrcode
|
|
||||||
psutil
|
|
||||||
openpyxl
|
|
||||||
fastapi
|
|
||||||
uvicorn
|
|
||||||
pydantic
|
|
||||||
python-dotenv
|
|
||||||
httpx
|
|
||||||
Reference in new issue
Block a user