MAJ main.py

This commit is contained in:
xavier committed 2026-10-05 11:05:17 +02:00
1 parent bba75ef660
commit 3c407a84d0
2 files changed
+178 -104

No files matched your search

+2 -16
View File
@@ -1,20 +1,6 @@
{ {
"e1464d87-5d53-4469-b21d-5d0dbeb4d515": { "8f36f77c-688a-4608-bfcc-38f36090ddd4": {
"title": "Discussion du 05/10 09:53", "title": "Discussion du 05/10 10:47",
"pinned": false,
"messages": [
{
"user": "Génére un fichier kubejs pour améliorer les botany pots et leur vitesse de production.",
"ia": "Erreur Pipeline Minecraft: 'response'"
},
{
"user": "Génére un fichier kubejs pour améliorer les botany pots et leur vitesse de production.",
"ia": "Voici la traduction en français, tout en conservant les blocs de code, les noms de variables et les commandes Minecraft inchangés :\n\n```bash\n#!/usr/bin/kubectl run --replicas=1 --expose-port=8080 --name=botany_pots_enhancer --image=registry.apps.example.com/botany_pots_enhancer --namespace=example --restart=always\n```\n\nCette ligne de commande utilise `kubectl`, un outil de ligne de commande pour gérer Kubernetes, pour déployer une application. Voici une explication des options utilisées :\n\n- `run` : crée et démarre un nouveau pod ou un déploiement.\n- `--replicas=1` : spécifie que le déploiement doit contenir 1 réplica.\n- `--expose-port=8080` : expose le port 8080 de l'application pour qu'il soit accessible.\n- `--name=botany_pots_enhancer` : nomme le déploiement \"botany_pots_enhancer\".\n- `--image=registry.apps.example.com/botany_pots_enhancer` : spécifie l'image Docker à utiliser pour le déploiement.\n- `--namespace=example` : déploie le déploiement dans le namespace \"example\".\n- `--restart=always` : assure que le pod est redémarré en cas de panne."
}
]
},
"9dc0f3cf-5f41-452c-bfba-12eb43da4cb2": {
"title": "Discussion du 05/10 10:06",
"pinned": false, "pinned": false,
"messages": [] "messages": []
} }
+176 -88
View File
@@ -9,64 +9,92 @@ from dotenv import load_dotenv
load_dotenv() load_dotenv()
# --- 1. GESTION DE L'HISTORIQUE (EPINGLAGE, RENOMMAGE, SUPPRESSION) --- # --- 0. INITIALISATION DES DOSSIERS (BDD & LOGS) ---
os.makedirs("sessions", exist_ok=True)
os.makedirs("logs", exist_ok=True)
def log_event(session_id, action, level="INFO"):
"""Enregistre chaque action dans un fichier texte dédié à la session."""
sid = session_id if session_id else "system"
log_file = f"logs/session_{sid}.log"
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(log_file, "a", encoding="utf-8") as f:
f.write(f"[{timestamp}] [{level}] {action}\n")
# --- 1. GESTION DE L'HISTORIQUE (INDEX & SESSIONS) ---
HISTORY_FILE = "history.json" HISTORY_FILE = "history.json"
def load_history(): def load_index():
if not os.path.exists(HISTORY_FILE): return {} if not os.path.exists(HISTORY_FILE): return {}
with open(HISTORY_FILE, "r", encoding="utf-8") as f: with open(HISTORY_FILE, "r", encoding="utf-8") as f:
try: return json.load(f) try:
idx = json.load(f)
# Migration automatique si l'ancien format (avec "messages" dedans) est détecté
for sid, data in list(idx.items()):
if "messages" in data:
save_session_data(sid, data["messages"])
del data["messages"]
return idx
except: return {} except: return {}
def save_history(history_data): def save_index(idx):
with open(HISTORY_FILE, "w", encoding="utf-8") as f: with open(HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump(history_data, f, indent=4, ensure_ascii=False) 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(): def create_new_session():
session_id = str(uuid.uuid4()) session_id = str(uuid.uuid4())
history = load_history() idx = load_index()
history[session_id] = {"title": f"Discussion du {datetime.now().strftime('%d/%m %H:%M')}", "pinned": False, "messages": []} idx[session_id] = {"title": f"Discussion du {datetime.now().strftime('%d/%m %H:%M')}", "pinned": False}
save_history(history) save_index(idx)
save_session_data(session_id, [])
log_event(session_id, "Nouvelle session initialisée.")
return session_id return session_id
def render_history_list(): def render_history_list():
"""Génère la liste HTML de l'historique avec les boutons d'action.""" idx = load_index()
history = load_history() sorted_sessions = sorted(idx.items(), key=lambda x: (not x[1].get('pinned', False), x[1]['title']), reverse=False)
sorted_sessions = sorted(history.items(), key=lambda x: (not x[1].get('pinned', False), x[1]['title']), reverse=False)
items = [
Button("➕ Nouvelle Discussion", cls="history-btn w-full mb-3 text-center", style="background:#00e5ff; color:#0f172a; font-weight:bold;", hx_get="/session/new", hx_target="#chat-history")
]
items = []
for sid, data in sorted_sessions: for sid, data in sorted_sessions:
pin_icon = "📍" if data.get('pinned') else "📌" pin_icon = "📍" if data.get('pinned') else "📌"
pin_color = "#10b981" if data.get('pinned') else "#94a3b8" pin_color = "#10b981" if data.get('pinned') else "#94a3b8"
item = Div( item = Div(
Div(data['title'], cls="truncate flex-grow cursor-pointer"), Div(data['title'], cls="truncate flex-grow cursor-pointer hover:text-cyan-400", hx_get=f"/session/load/{sid}", hx_target="#chat-history"),
Div( Div(
# Bouton Renommer Button("✏️", cls="text-xs mx-1 hover:text-white", onclick=f"let name = prompt('Nouveau nom:'); if(name) {{ htmx.ajax('POST', '/history/rename/{sid}', {{values: {{title: name}}, target: '#history-list-container'}}); }}"),
Button("✏️", cls="text-xs mx-1 hover:text-white", 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"),
onclick=f"let name = prompt('Nouveau nom:'); if(name) {{ htmx.ajax('POST', '/history/rename/{sid}', {{values: {{title: name}}, target: '#history-list'}}); }}"), 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'}}); }}"),
# Bouton Epingler
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"),
# Bouton Supprimer (Nouveau)
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'}}); }}"),
cls="flex-shrink-0" cls="flex-shrink-0"
), ),
cls="flex justify-between items-center history-btn w-full mb-1" cls="flex justify-between items-center history-btn w-full mb-1"
) )
items.append(item) items.append(item)
return Div(*items, id="history-list", style="flex-grow: 1; overflow-y: auto;") return Div(*items, id="history-list-container", hx_swap_oob="true")
# --- 2. GESTION DU CATALOGUE (GENERIQUE) --- # --- 2. GESTION DU CATALOGUE (GENERIQUE & OPENROUTER) ---
MODELS_DATA = [] MODELS_DATA = []
def init_models(): def init_models():
global MODELS_DATA global MODELS_DATA
MODELS_DATA = [ MODELS_DATA = [
{"id": "groq|llama3-8b-8192", "name": "Groq - Llama 3 (8B)", "is_free": True}, {"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}, {"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} {"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: try:
res = requests.get("https://openrouter.ai/api/v1/models", timeout=5) res = requests.get("https://openrouter.ai/api/v1/models", timeout=5)
@@ -75,7 +103,8 @@ def init_models():
m["id"] = f"openrouter|{m['id']}" m["id"] = f"openrouter|{m['id']}"
m["name"] = f"OR - {m['name']}" m["name"] = f"OR - {m['name']}"
MODELS_DATA.append(m) MODELS_DATA.append(m)
except: pass except Exception as e:
log_event("system", f"Erreur chargement catalogue OpenRouter : {e}", "ERROR")
init_models() init_models()
def get_model_options(filter_type="all"): def get_model_options(filter_type="all"):
@@ -98,7 +127,8 @@ def get_budget():
return f"{data.get('total_credits', 0) - data.get('total_usage', 0):.4f} $" return f"{data.get('total_credits', 0) - data.get('total_usage', 0):.4f} $"
except: return "Erreur lecture" except: return "Erreur lecture"
def handle_api_error(provider, raw_error): def handle_api_error(session_id, provider, raw_error):
log_event(session_id, f"Erreur API {provider} : {raw_error}", "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." 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: 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"] 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"]
@@ -106,54 +136,57 @@ def handle_api_error(provider, raw_error):
except: except:
return f"⚠️ Erreur brute ({provider}) : {raw_error}" return f"⚠️ Erreur brute ({provider}) : {raw_error}"
async def async_ask_llm(provider, actual_model, msg): async def async_ask_llm(session_id, provider, actual_model, msg):
return await asyncio.to_thread(ask_llm, provider, actual_model, msg) return await asyncio.to_thread(ask_llm, session_id, provider, actual_model, msg)
def ask_llm(provider, actual_model, msg): def ask_llm(session_id, provider, actual_model, msg):
log_event(session_id, f"Interrogation API distantes : {provider} | Modèle : {actual_model}")
try: try:
if provider == "openrouter": if provider == "openrouter":
res = requests.post("https://openrouter.ai/api/v1/chat/completions", headers={"Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}"}, json={"model": actual_model, "messages": [{"role": "user", "content": msg}]}) res = requests.post("https://openrouter.ai/api/v1/chat/completions", headers={"Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}"}, json={"model": actual_model, "messages": [{"role": "user", "content": msg}]})
if res.status_code != 200: return handle_api_error(provider, res.text) if res.status_code != 200: return handle_api_error(session_id, provider, res.text)
return res.json()["choices"][0]["message"]["content"] return res.json()["choices"][0]["message"]["content"]
elif provider == "groq": elif provider == "groq":
res = requests.post("https://api.groq.com/openai/v1/chat/completions", headers={"Authorization": f"Bearer {os.getenv('GROQ_API_KEY')}"}, json={"model": actual_model, "messages": [{"role": "user", "content": msg}]}) res = requests.post("https://api.groq.com/openai/v1/chat/completions", headers={"Authorization": f"Bearer {os.getenv('GROQ_API_KEY')}"}, json={"model": actual_model, "messages": [{"role": "user", "content": msg}]})
if res.status_code != 200: return handle_api_error(provider, res.text) if res.status_code != 200: return handle_api_error(session_id, provider, res.text)
return res.json()["choices"][0]["message"]["content"] return res.json()["choices"][0]["message"]["content"]
elif provider == "gemini": elif provider == "gemini":
res = requests.post(f"https://generativelanguage.googleapis.com/v1beta/models/{actual_model}:generateContent?key={os.getenv('GEMINI_API_KEY')}", json={"contents": [{"parts": [{"text": msg}]}]}) res = requests.post(f"https://generativelanguage.googleapis.com/v1beta/models/{actual_model}:generateContent?key={os.getenv('GEMINI_API_KEY')}", json={"contents": [{"parts": [{"text": msg}]}]})
if res.status_code != 200: return handle_api_error(provider, res.text) if res.status_code != 200: return handle_api_error(session_id, provider, res.text)
return res.json()["candidates"][0]["content"]["parts"][0]["text"] return res.json()["candidates"][0]["content"]["parts"][0]["text"]
return handle_api_error(provider, "Fournisseur non implémenté.") return handle_api_error(session_id, provider, "Fournisseur non implémenté.")
except Exception as e: except Exception as e:
return handle_api_error(provider, str(e)) return handle_api_error(session_id, provider, str(e))
# --- 4. PIPELINES DE TRAITEMENT --- # --- 4. PIPELINES DE TRAITEMENT ---
def pipeline_minecraft(raw_msg): def pipeline_minecraft(session_id, raw_msg):
log_event(session_id, "Lancement Pipeline 100% Local Minecraft (Qwen -> Phuzzy -> Qwen)")
try: 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"] 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"] 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"] 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" return final_fr, "Qwen ➔ Phuzzy ➔ Qwen"
except Exception as e: except Exception as e:
return handle_api_error("Ollama Local", str(e)), "Erreur Locale" return handle_api_error(session_id, "Ollama Local", str(e)), "Erreur Locale"
async def pipeline_consolidated(raw_msg): async def pipeline_consolidated(session_id, raw_msg):
log_event(session_id, "Lancement Pipeline Consolidation Multi-IA")
try: 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"] 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( rep_groq, rep_gemini = await asyncio.gather(
async_ask_llm("groq", "llama3-8b-8192", opt_prompt), async_ask_llm(session_id, "groq", "llama3-8b-8192", opt_prompt),
async_ask_llm("gemini", "gemini-1.5-flash", 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." 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_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"] 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" return final_res, "Qwen ➔ [Groq + Gemini] ➔ Synthèse Qwen"
except Exception as e: except Exception as e:
return handle_api_error("Pipeline Multi-IA", str(e)), "Erreur Consolidation" return handle_api_error(session_id, "Pipeline Multi-IA", str(e)), "Erreur Consolidation"
# --- 5. INTERFACE UTILISATEUR & ROUTES --- # --- 5. INTERFACE UTILISATEUR & ROUTES ---
@@ -180,7 +213,10 @@ theme_hdrs = [
.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; } .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;} .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;}
.htmx-request .htmx-indicator { display: block; } .htmx-request .htmx-indicator { display: block; }
.history-btn { background: #2a2a35; color: #e2e8f0; border: 1px solid #444; padding: 8px; border-radius: 5px; } .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; text-decoration: none; transition: 0.2s;}
.export-btn:hover { background: #00e5ff; color: #0f172a; }
"""), """),
Script(""" Script("""
function updateUI() { function updateUI() {
@@ -192,8 +228,13 @@ theme_hdrs = [
function updateFileCount(input) { function updateFileCount(input) {
let label = document.getElementById('file-label-text'); let label = document.getElementById('file-label-text');
if(input.files.length > 0) { label.innerText = `📎 ${input.files.length} fichier(s)`; label.style.color = '#00e5ff'; } if(input && input.files && input.files.length > 0) {
else { label.innerText = '📎 Fichiers'; label.style.color = '#94a3b8'; } label.innerText = `📎 ${input.files.length} fichier(s)`;
label.style.color = '#00e5ff';
} else {
label.innerText = '📎 Fichiers';
label.style.color = '#94a3b8';
}
} }
htmx.onLoad(function(content) { htmx.onLoad(function(content) {
@@ -209,46 +250,66 @@ theme_hdrs = [
app, rt = fast_app(hdrs=theme_hdrs) 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, "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}') @rt('/history/pin/{sid}')
def post_pin(sid: str): def post_pin(sid: str):
history = load_history() idx = load_index()
if sid in history: if sid in idx:
history[sid]['pinned'] = not history[sid].get('pinned', False) idx[sid]['pinned'] = not idx[sid].get('pinned', False)
save_history(history) save_index(idx)
log_event(sid, "Épinglage modifié.")
return render_history_list() return render_history_list()
@rt('/history/rename/{sid}') @rt('/history/rename/{sid}')
def post_rename(sid: str, title: str): def post_rename(sid: str, title: str):
history = load_history() idx = load_index()
if sid in history: if sid in idx:
history[sid]['title'] = title idx[sid]['title'] = title
save_history(history) save_index(idx)
log_event(sid, f"Renommé en: {title}")
return render_history_list() return render_history_list()
@rt('/history/delete/{sid}') @rt('/history/delete/{sid}')
def post_delete(sid: str): def post_delete(sid: str):
"""Nouveau : Route pour la suppression d'une conversation.""" idx = load_index()
history = load_history() if sid in idx:
if sid in history: del idx[sid]
del history[sid] save_index(idx)
save_history(history) if os.path.exists(f"sessions/{sid}.json"): os.remove(f"sessions/{sid}.json")
log_event(sid, "Session supprimée définitivement.", "WARNING")
return render_history_list() return render_history_list()
@rt('/export_models') @rt('/export_models')
def get_export_models(): def get():
"""Nouveau : Route pour extraire tous les modèles, trier par type et télécharger en TXT.""" log_event("system", "Demande d'exportation du catalogue de modèles.")
txt_content = "=== CATALOGUE DES MODÈLES (OPENROUTER & LOCAUX) ===\n\n" now = datetime.now()
date_str = now.strftime('%d/%m/%Y à %H:%M:%S')
file_date = now.strftime('%Y%m%d_%H%M%S')
try: txt_content = f"=== CATALOGUE COMPLET DES MODÈLES (Généré le {date_str}) ===\n\n"
res = requests.get("https://openrouter.ai/api/v1/models", timeout=10)
if res.status_code == 200: for m in MODELS_DATA:
for m in res.json().get("data", []):
name = m.get('name', 'Inconnu') name = m.get('name', 'Inconnu')
m_id = m.get('id', 'Inconnu') m_id = m.get('id', 'Inconnu')
desc = str(m.get('description', '')).replace('\n', ' ')
desc_courte = desc[:150] + "..." if len(desc) > 150 else desc
# Détection intelligente du domaine de spécialité
usage = "Général / Texte" usage = "Général / Texte"
arch_mod = str(m.get('architecture', {}).get('modality', '')).lower() arch_mod = str(m.get('architecture', {}).get('modality', '')).lower()
m_id_lower = m_id.lower() m_id_lower = m_id.lower()
@@ -262,17 +323,33 @@ def get_export_models():
elif "rp" in m_id_lower or "uncensored" in m_id_lower or "roleplay" in m_id_lower: elif "rp" in m_id_lower or "uncensored" in m_id_lower or "roleplay" in m_id_lower:
usage = "Non-censuré / Roleplay" usage = "Non-censuré / Roleplay"
# Détection du prix is_free = m.get("is_free")
if is_free is None:
pricing = m.get('pricing', {}) pricing = m.get('pricing', {})
p_prompt = float(pricing.get('prompt', -1)) p_prompt = float(pricing.get('prompt', -1))
p_comp = float(pricing.get('completion', -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})" 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"
txt_content += f"Nom : {name}\nID : {m_id}\nTarif : {cout}\nSpécialité : {usage}\nDescription : {desc_courte}\n{'-'*60}\n" desc = str(m.get('description', '')).replace('\n', ' ')
except Exception as e: desc_courte = desc[:150] + "..." if len(desc) > 150 else desc
txt_content += f"Erreur de récupération : {e}\n" desc_fr = desc_courte
return Response(txt_content, media_type="text/plain", headers={"Content-Disposition": "attachment; filename=modeles_ia.txt"}) if "openrouter" in m_id and desc_courte:
try:
# Appel direct Google Translate
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=2)
if trans_res.status_code == 200:
desc_fr = "".join([x[0] for x in trans_res.json()[0]])
except: pass
if not desc_fr: desc_fr = "Aucune description fournie."
txt_content += f"Nom : {name}\nID : {m_id}\nTarif : {cout}\nSpécialité : {usage}\nDescription : {desc_fr}\n{'-'*60}\n"
return Response(txt_content, media_type="text/plain", headers={"Content-Disposition": f"attachment; filename=modeles_ia_{file_date}.txt"})
@rt('/') @rt('/')
@@ -290,8 +367,8 @@ def get():
style="display:flex; flex-direction:column; flex-grow:1; margin-bottom: 10px; overflow:hidden;" style="display:flex; flex-direction:column; flex-grow:1; margin-bottom: 10px; overflow:hidden;"
), ),
# Nouveau bouton d'extraction des modèles # hx_disable=True autorise le navigateur à télécharger le fichier généré
A(Button("📥 Exporter Modèles (.txt)", cls="history-btn w-full mt-2 text-center", style="background:#0f172a; border-color:#00e5ff; color:#00e5ff; font-weight:bold;"), href="/export_models", download="modeles.txt"), A("📥 Exporter Modèles (.txt)", cls="export-btn w-full mt-2", href="/export_models", hx_disable=True),
Div( Div(
P("BUDGET OPENROUTER", style="color:#94a3b8; font-size:10px; font-weight:bold; margin-bottom:5px;"), P("BUDGET OPENROUTER", style="color:#94a3b8; font-size:10px; font-weight:bold; margin-bottom:5px;"),
@@ -305,7 +382,7 @@ def get():
Div("⚙️ Traitement de l'Architecture IA en cours...", id="loading-tracker", cls="htmx-indicator"), Div("⚙️ Traitement de l'Architecture IA en cours...", id="loading-tracker", cls="htmx-indicator"),
Form( Form(
Input(type="hidden", name="session_id", value=session_id), Input(type="hidden", name="session_id", value=session_id, id="current-session-id"),
Div( Div(
Label(Input(type="radio", name="context_type", value="generic", checked=True, onchange="updateUI()"), " 💻 Code Générique (Modèle Unique)"), 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="consolidated", onchange="updateUI()"), " 🧠 Consolidation (Multi-IA ➔ Qwen)"),
@@ -318,14 +395,14 @@ def get():
cls="flex gap-2 mb-2 w-full", id="model-selection-area" cls="flex gap-2 mb-2 w-full", id="model-selection-area"
), ),
Div( Div(
# Le tag multiple permet nativement de sélectionner 10, 50, ou 100+ fichiers simultanément
Input(type="file", name="fichiers", id="file-upload", multiple=True, style="display:none;", onchange="updateFileCount(this)"), Input(type="file", name="fichiers", id="file-upload", multiple=True, style="display:none;", onchange="updateFileCount(this)"),
Label(Span("📎 Fichiers", id="file-label-text"), _for="file-upload", cls="file-upload-btn"), Label(Span("📎 Fichiers", 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), Input(type="text", name="msg", placeholder="Insérez votre requête...", cls="input-box", required=True),
Button("Envoyer", type="submit", cls="send-btn"), Button("Envoyer", type="submit", cls="send-btn"),
cls="input-row" cls="input-row"
), ),
hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#loading-tracker", enctype="multipart/form-data" hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#loading-tracker", enctype="multipart/form-data",
**{"hx-on:htmx:afterRequest": "if(event.detail.successful) { this.reset(); updateFileCount(document.getElementById('file-upload')); }"}
), ),
cls="main-content" cls="main-content"
), ),
@@ -338,6 +415,8 @@ def get(filter_type: str): return get_model_options(filter_type)
@rt('/chat') @rt('/chat')
async def post(msg: str, model_id: str, session_id: str, context_type: str = "generic", fichiers: list[UploadFile] = None): async def post(msg: str, model_id: str, session_id: str, context_type: str = "generic", fichiers: list[UploadFile] = None):
log_event(session_id, f"Envoi d'un message (Contexte: {context_type})")
files_context = "" files_context = ""
noms_fichiers = [] noms_fichiers = []
if fichiers: if fichiers:
@@ -345,34 +424,43 @@ async def post(msg: str, model_id: str, session_id: str, context_type: str = "ge
if f.filename: if f.filename:
noms_fichiers.append(f.filename) noms_fichiers.append(f.filename)
try: files_context += f"\n--- {f.filename} ---\n{(await f.read()).decode('utf-8')}\n" try: files_context += f"\n--- {f.filename} ---\n{(await f.read()).decode('utf-8')}\n"
except: files_context += f"\n--- {f.filename} (Binaire ignoré) ---\n" except:
files_context += f"\n--- {f.filename} (Binaire ignoré) ---\n"
log_event(session_id, f"Fichier non lisible : {f.filename}", "WARNING")
full_req = f"Fichiers fournis:\n{files_context}\nDemande: {msg}" if files_context else msg full_req = f"Fichiers fournis:\n{files_context}\nDemande: {msg}" if files_context else msg
if context_type == "minecraft": if context_type == "minecraft":
ia_reponse, pipeline_info = pipeline_minecraft(full_req) ia_reponse, pipeline_info = pipeline_minecraft(session_id, full_req)
nom_affichage = "Local - Phuzzy/Minecraft" nom_affichage = "Local - Phuzzy/Minecraft"
elif context_type == "consolidated": elif context_type == "consolidated":
ia_reponse, pipeline_info = await pipeline_consolidated(full_req) ia_reponse, pipeline_info = await pipeline_consolidated(session_id, full_req)
nom_affichage = "Qwen Synthèse (via Groq/Gemini)" nom_affichage = "Qwen Synthèse (via Groq/Gemini)"
else: 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"] 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) parts = model_id.split("|", 1)
provider, actual_model = parts[0], parts[1] if len(parts) > 1 else model_id provider, actual_model = parts[0], parts[1] if len(parts) > 1 else model_id
ia_reponse = ask_llm(provider, actual_model, opt_prompt) ia_reponse = ask_llm(session_id, provider, actual_model, opt_prompt)
nom_affichage = f"{provider.capitalize()} - {actual_model.split('/')[-1]}" nom_affichage = f"{provider.capitalize()} - {actual_model.split('/')[-1]}"
pipeline_info = "Qwen ➔ API Distante Unique" pipeline_info = "Qwen ➔ API Distante Unique"
history = load_history()
if session_id in history:
history[session_id]["messages"].append({"user": msg, "ia": ia_reponse})
save_history(history)
info_fichiers = f"<br><span style='color:#a855f7; font-size:10px;'>📎 {len(noms_fichiers)} fichier(s) joint(s)</span>" if noms_fichiers else "" 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>" info_local = f"<br><span style='color:#38bdf8; font-size:10px;'>⚙️ Pipeline : {pipeline_info}</span>"
# Séparation et sauvegarde des données complètes pour la session
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, "Réponse sauvegardée avec succès.")
chat_bubble = Div( chat_bubble = Div(
P("Vous", cls="msg-user"), Div(NotStr(f"{msg}{info_fichiers}{info_local}"), cls="bubble-user"), 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") P(f"AETHAS38 ({nom_affichage})", cls="msg-ia"), Div(ia_reponse, cls="bubble-ia")