Compare commits

...
4 Commits
4 changed files with 181 additions and 50 deletions

No files matched your search

+68 -10
View File
@@ -3,7 +3,7 @@ from fastapi.responses import RedirectResponse, FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List from typing import List
import os, json, asyncio, shutil import os, json, asyncio, shutil, base64, io
from datetime import datetime from datetime import datetime
import pytz import pytz
@@ -73,7 +73,8 @@ def login(login_data: LoginRequest, response: Response, db: Session = Depends(ge
user = db.query(User).filter(User.username == login_data.username).first() user = db.query(User).filter(User.username == login_data.username).first()
if not user or not verify_password(login_data.password, user.hashed_password): raise HTTPException(status_code=401, detail="Identifiants incorrects.") if not user or not verify_password(login_data.password, user.hashed_password): raise HTTPException(status_code=401, detail="Identifiants incorrects.")
if not verify_totp(user.totp_secret, login_data.totp_code): raise HTTPException(status_code=401, detail="2FA invalide.") if not verify_totp(user.totp_secret, login_data.totp_code): raise HTTPException(status_code=401, detail="2FA invalide.")
response.set_cookie(key="session_token", value=create_access_token(data={"sub": user.username}), httponly=True, max_age=3600, samesite="lax") # Correction : Extension de la durée de session à 7 jours (604800 secondes) pour éviter les erreurs 401 intempestives
response.set_cookie(key="session_token", value=create_access_token(data={"sub": user.username}), httponly=True, max_age=604800, samesite="lax")
return {"message": "Connexion réussie"} return {"message": "Connexion réussie"}
@app.get("/dashboard") @app.get("/dashboard")
@@ -143,21 +144,78 @@ def get_messages(project_id: int, db: Session = Depends(get_db), current_user: U
@app.post("/api/projects/{project_id}/messages", response_model=List[MessageResponse]) @app.post("/api/projects/{project_id}/messages", response_model=List[MessageResponse])
async def create_message(project_id: int, message: MessageCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): async def create_message(project_id: int, message: MessageCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
final_content = message.content extracted_files_data = []
if message.files: files_names = []
files_text = ""
for f in message.files:
files_text += f"\n\n[Fichier attaché : {f.name}]\n```\n{f.content}\n```"
final_content = message.content + files_text
db.add(Message(role=message.role, content=final_content, project_id=project_id)) if message.files:
for f in message.files:
files_names.append(f.name)
content = f.content
if content.startswith("data:"):
try:
header, b64data = content.split(",", 1)
file_bytes = base64.b64decode(b64data)
ext = f.name.split('.')[-1].lower()
extracted_text = ""
if ext == 'pdf':
try:
import PyPDF2
reader = PyPDF2.PdfReader(io.BytesIO(file_bytes))
extracted_text = "\n".join([page.extract_text() for page in reader.pages if page.extract_text()])
except ImportError:
extracted_text = "[Erreur: L'administrateur doit exécuter 'pip install PyPDF2' sur le serveur pour lire les PDF.]"
elif ext in ['xls', 'xlsx', 'xlsm', 'xlsb']:
try:
import openpyxl
wb = openpyxl.load_workbook(io.BytesIO(file_bytes), data_only=False)
for sheet_name in wb.sheetnames:
sheet = wb[sheet_name]
extracted_text += f"\n--- Feuille : {sheet_name} ---\n"
for row in sheet.iter_rows(values_only=True):
row_vals = [str(cell) if cell is not None else "" for cell in row]
if any(row_vals):
extracted_text += "\t".join(row_vals) + "\n"
if ext in ['xlsm', 'xlsb', 'xls']:
try:
from oletools.olevba import VBA_Parser
vbaparser = VBA_Parser("filename", data=file_bytes)
if vbaparser.detect_vba_macros():
extracted_text += "\n\n--- MACROS VBA DETECTEES ---\n"
for (filename, stream_path, vba_filename, vba_code) in vbaparser.extract_macros():
extracted_text += f"\n// Module: {vba_filename}\n{vba_code}\n"
except ImportError:
extracted_text += "\n[Extraction VBA impossible: L'administrateur doit exécuter 'pip install oletools' sur le serveur.]\n"
except Exception as e:
extracted_text += f"\n[Erreur de lecture VBA interne: {str(e)}]\n"
except ImportError:
extracted_text = "[Erreur: L'administrateur doit exécuter 'pip install openpyxl' sur le serveur pour lire Excel.]"
else:
extracted_text = file_bytes.decode('utf-8', errors='replace')
extracted_files_data.append({"name": f.name, "content": extracted_text})
except Exception as e:
extracted_files_data.append({"name": f.name, "content": f"[ERREUR DE DECODAGE: {str(e)}]"})
else:
extracted_files_data.append({"name": f.name, "content": content})
# On ne stocke plus le contenu brut des fichiers en DB pour éviter d'exploser le contexte des requêtes suivantes
db_content = message.content
if files_names:
db_content += f"\n\n[Fichiers joints pour analyse : {', '.join(files_names)}]"
db.add(Message(role=message.role, content=db_content, project_id=project_id))
db.commit() db.commit()
history = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all() history = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
settings = db.query(SystemSettings).first() settings = db.query(SystemSettings).first()
conf = message.config.dict() if message.config else {"workers": ["gemini-3.5-flash-lite"]} conf = message.config.dict() if message.config else {"workers": ["gemini-3.5-flash-lite"]}
ai_resp = await run_orchestrator(db, history, settings, conf) # Transmission des données de fichiers en mémoire vive à l'orchestrateur (Map-Reduce)
ai_resp = await run_orchestrator(db, history, settings, conf, extracted_files_data)
db.add(Message(role="assistant", content=ai_resp, project_id=project_id)) db.add(Message(role="assistant", content=ai_resp, project_id=project_id))
db.commit() db.commit()
+57 -7
View File
@@ -16,10 +16,9 @@ def determine_domain(model_id: str) -> str:
return "Texte Polyvalent" return "Texte Polyvalent"
async def translate_en_to_fr(client: httpx.AsyncClient, text: str) -> str: async def translate_en_to_fr(client: httpx.AsyncClient, text: str) -> str:
"""Traduit automatiquement l'anglais vers le français via l'API publique Google Translate."""
if not text: return "Aucune description fournie." if not text: return "Aucune description fournie."
try: try:
short_text = text[:300].strip() # Limite stricte pour l'API gratuite short_text = text[:300].strip()
url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=fr&dt=t&q={urllib.parse.quote(short_text)}" url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=fr&dt=t&q={urllib.parse.quote(short_text)}"
resp = await client.get(url, timeout=4.0) resp = await client.get(url, timeout=4.0)
if resp.status_code == 200: if resp.status_code == 200:
@@ -195,19 +194,68 @@ async def ask_agent(client, model_id, messages, provider="openrouter"):
kwargs = {"model": model_id, "messages": messages} kwargs = {"model": model_id, "messages": messages}
if provider == "openrouter": if provider == "openrouter":
kwargs["extra_headers"] = {"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "AETHAS38 Orchestrator"} kwargs["extra_headers"] = {"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "AETHAS38 Orchestrator"}
# Activation du plugin natif de compression d'OpenRouter pour éviter le dépassement de contexte
kwargs["extra_body"] = {"plugins": [{"id": "context-compression"}]}
resp = await client.chat.completions.create(**kwargs) resp = await client.chat.completions.create(**kwargs)
return resp.choices[0].message.content return resp.choices[0].message.content
async def run_orchestrator(db: Session, history: list, settings: SystemSettings, config: dict) -> str: async def run_orchestrator(db: Session, history: list, settings: SystemSettings, config: dict, extracted_files: list = None) -> str:
workers = config.get("workers", ["gemini-3.5-flash-lite"]) workers = config.get("workers", ["gemini-3.5-flash-lite"])
user_prompt = history[-1].content user_prompt = history[-1].content
original_user_text = user_prompt.split("\n\n[Fichiers joints")[0] if "[Fichiers joints" in user_prompt else user_prompt
formatted_history = [{"role": msg.role, "content": msg.content} for msg in history[:-1]] formatted_history = [{"role": msg.role, "content": msg.content} for msg in history[:-1]]
final_response = "" final_response = ""
try: try:
p_mod = config.get("prompter", "gemini-3.5-flash-lite") p_mod = config.get("prompter")
if not p_mod: p_mod = "gemini-3.5-flash-lite"
p_client, p_prov = get_client_for_model(db, p_mod, settings) p_client, p_prov = get_client_for_model(db, p_mod, settings)
prompt_system = "You are an expert prompt engineer. Translate and optimize the user request into clear, precise English tailored for AI execution."
# --- WORKFLOW MAP-REDUCE : PRÉ-TRAITEMENT SÉQUENTIEL & CHUNKING ---
files_context = ""
if extracted_files:
async def process_single_file(f):
file_sys = "You are an expert data analyst and senior developer. Extract the most important technical information from the file without losing critical code syntax."
content = f['content']
chunk_size = 150000 # Environ 35k à 40k tokens par morceau pour rester très large par rapport aux limites
# CHUNKING : Découpage intelligent si le fichier est massif
if len(content) > chunk_size:
chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
chunk_analyses = []
for idx, chunk in enumerate(chunks):
file_prompt = f"Demande de l'utilisateur : '{original_user_text}'.\n\nPartie {idx+1}/{len(chunks)} du fichier '{f['name']}'. Analysez, extrayez et résumez le code, VBA, SQL ou les données pertinentes.\n\nContenu :\n```\n{chunk}\n```"
try:
analysis = await ask_agent(p_client, p_mod, [{"role": "system", "content": file_sys}, {"role": "user", "content": file_prompt}], p_prov)
chunk_analyses.append(analysis)
except Exception as e:
chunk_analyses.append(f"[Erreur sur la partie {idx+1}: {str(e)}]")
await asyncio.sleep(1.5) # Pause anti-spam (429) entre les morceaux
return f"\n\n--- Extraction du fichier {f['name']} (en {len(chunks)} parties) ---\n" + "\n".join(chunk_analyses)
else:
file_prompt = f"Demande de l'utilisateur : '{original_user_text}'.\n\nAnalysez le fichier ci-dessous. Extrayez, résumez et conservez méticuleusement tout le code, les macros VBA, les requêtes SQL, ou les données métier pertinentes pour répondre à la demande.\n\nFichier : {f['name']}\nContenu :\n```\n{content}\n```"
try:
analysis = await ask_agent(p_client, p_mod, [{"role": "system", "content": file_sys}, {"role": "user", "content": file_prompt}], p_prov)
return f"\n\n--- Extraction du fichier {f['name']} ---\n{analysis}"
except Exception as e:
return f"\n\n--- Erreur sur {f['name']} ---\n{str(e)}"
file_analyses = []
for f in extracted_files:
analysis = await process_single_file(f)
file_analyses.append(analysis)
# SÉQUENÇAGE : Pause de 1.5 seconde entre les fichiers pour éviter l'erreur 429
await asyncio.sleep(1.5)
files_context = "".join(file_analyses)
user_prompt = f"{original_user_text}\n\nVoici les données pré-traitées des fichiers joints :\n{files_context}"
# --- OPTIMISATION & TRADUCTION ---
prompt_system = "You are an expert prompt engineer. Translate and optimize the user request and any file context into clear, precise English tailored for AI execution. Keep all code blocks intact."
optimized = await ask_agent(p_client, p_mod, [{"role": "system", "content": prompt_system}, {"role": "user", "content": user_prompt}], p_prov) optimized = await ask_agent(p_client, p_mod, [{"role": "system", "content": prompt_system}, {"role": "user", "content": user_prompt}], p_prov)
if len(workers) == 1: if len(workers) == 1:
@@ -222,7 +270,9 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings,
w_tasks.append(ask_agent(w_client, w, formatted_history + [{"role": "user", "content": optimized}], w_prov)) w_tasks.append(ask_agent(w_client, w, formatted_history + [{"role": "user", "content": optimized}], w_prov))
responses = await asyncio.gather(*w_tasks, return_exceptions=True) responses = await asyncio.gather(*w_tasks, return_exceptions=True)
c_mod = config.get("concatenator", "gemini-3.5-flash-lite") c_mod = config.get("concatenator")
if not c_mod: c_mod = "gemini-3.5-flash-lite"
c_client, c_prov = get_client_for_model(db, c_mod, settings) c_client, c_prov = get_client_for_model(db, c_mod, settings)
concat_system = ( concat_system = (
@@ -233,7 +283,7 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings,
"You may translate code comments into French if appropriate, but leave code syntax strictly intact." "You may translate code comments into French if appropriate, but leave code syntax strictly intact."
) )
synth = f"User Request: {user_prompt}\n\n" + "\n".join([f"--- EXPERT {i+1} ---\n{r}" for i, r in enumerate(responses)]) synth = f"User Request: {original_user_text}\n\n" + "\n".join([f"--- EXPERT {i+1} ---\n{str(r)}" for i, r in enumerate(responses)])
final_response = await ask_agent(c_client, c_mod, [{"role": "system", "content": concat_system}, {"role": "user", "content": synth}], c_prov) final_response = await ask_agent(c_client, c_mod, [{"role": "system", "content": concat_system}, {"role": "user", "content": synth}], c_prov)
except Exception as e: except Exception as e:
+53 -33
View File
@@ -293,7 +293,6 @@
</div> </div>
<div class="p-4 md:px-6 md:pt-6 pb-2 w-full max-w-7xl mx-auto flex-shrink-0"> <div class="p-4 md:px-6 md:pt-6 pb-2 w-full max-w-7xl mx-auto flex-shrink-0">
<!-- EN-TÊTE CONFIGURATION IA (AVEC BOUTONS IMPORT/EXPORT) -->
<div class="flex justify-between items-center mb-2"> <div class="flex justify-between items-center mb-2">
<span class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Configuration des IA</span> <span class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Configuration des IA</span>
<div class="flex gap-2"> <div class="flex gap-2">
@@ -305,10 +304,8 @@
</div> </div>
</div> </div>
<!-- BARRE DE SÉLECTION DE STYLE TABULAIRE (9 COLONNES STRICTES) --> <!-- BARRE DE SÉLECTION -->
<div class="grid grid-cols-9 border border-black bg-gray-900 rounded-lg overflow-hidden text-center text-xs font-bold shadow-lg mb-4"> <div class="grid grid-cols-9 border border-black bg-gray-900 rounded-lg overflow-hidden text-center text-xs font-bold shadow-lg mb-4">
<!-- 1. PAYANT / GRATUIT -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center"> <div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-gray-400 mb-1">PRIX</label> <label class="block text-[9px] text-gray-400 mb-1">PRIX</label>
<select v-model="colPricing" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none"> <select v-model="colPricing" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none">
@@ -318,7 +315,6 @@
</select> </select>
</div> </div>
<!-- 2. TYPE (DOMAINE) -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center"> <div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-gray-400 mb-1">TYPE</label> <label class="block text-[9px] text-gray-400 mb-1">TYPE</label>
<select v-model="colDomain" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none"> <select v-model="colDomain" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none">
@@ -330,7 +326,6 @@
</select> </select>
</div> </div>
<!-- 3. TRAVAILLEUR 1 -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center"> <div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 1</label> <label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 1</label>
<select v-model="workers[0]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate"> <select v-model="workers[0]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
@@ -339,7 +334,6 @@
</select> </select>
</div> </div>
<!-- 4. TRAVAILLEUR 2 -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center"> <div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 2</label> <label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 2</label>
<select v-model="workers[1]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate"> <select v-model="workers[1]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
@@ -348,7 +342,6 @@
</select> </select>
</div> </div>
<!-- 5. TRAVAILLEUR 3 -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center"> <div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 3</label> <label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 3</label>
<select v-model="workers[2]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate"> <select v-model="workers[2]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
@@ -357,7 +350,6 @@
</select> </select>
</div> </div>
<!-- 6. TRAVAILLEUR 4 -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center"> <div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 4</label> <label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 4</label>
<select v-model="workers[3]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate"> <select v-model="workers[3]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
@@ -366,7 +358,6 @@
</select> </select>
</div> </div>
<!-- 7. TRAVAILLEUR 5 -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center"> <div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 5</label> <label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 5</label>
<select v-model="workers[4]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate"> <select v-model="workers[4]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
@@ -375,29 +366,25 @@
</select> </select>
</div> </div>
<!-- 8. PROMPTEUR -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center"> <div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-gray-400 mb-1">PROMPTEUR</label> <label class="block text-[9px] text-gray-400 mb-1">PROMPTEUR</label>
<select v-model="selectedPrompter" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate"> <select v-model="selectedPrompter" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
<option value="gemini-3.5-flash-lite">Gemini Flash</option> <option value="">(Par défaut)</option>
<option v-for="m in getFilteredModelsForCol(colPricing, colDomain, selectedPrompter)" :key="m.model_id" :value="m.model_id">{{ m.name || m.model_id }}</option> <option v-for="m in getFilteredModelsForCol(colPricing, colDomain, selectedPrompter)" :key="m.model_id" :value="m.model_id">{{ m.name || m.model_id }}</option>
</select> </select>
</div> </div>
<!-- 9. CONCATENEUR -->
<div class="p-1.5 bg-gray-800 flex flex-col justify-center"> <div class="p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-aethas-purple mb-1">CONCATENEUR</label> <label class="block text-[9px] text-aethas-purple mb-1">CONCATENEUR</label>
<select v-model="selectedConcatenator" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate"> <select v-model="selectedConcatenator" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
<option value="gemini-3.5-flash-lite">Gemini Flash</option> <option value="">(Par défaut)</option>
<option v-for="m in getFilteredModelsForCol(colPricing, colDomain, selectedConcatenator)" :key="m.model_id" :value="m.model_id">{{ m.name || m.model_id }}</option> <option v-for="m in getFilteredModelsForCol(colPricing, colDomain, selectedConcatenator)" :key="m.model_id" :value="m.model_id">{{ m.name || m.model_id }}</option>
</select> </select>
</div> </div>
</div> </div>
<!-- Champ de texte et boutons d'envoi / multi-fichiers --> <!-- CHAT INPUT -->
<div class="relative flex items-end bg-gray-800 border border-gray-600 rounded-2xl p-2 shadow-lg focus-within:border-aethas-cyan flex-col gap-2"> <div class="relative flex items-end bg-gray-800 border border-gray-600 rounded-2xl p-2 shadow-lg focus-within:border-aethas-cyan flex-col gap-2">
<!-- Liste des fichiers attachés -->
<div v-if="attachedFiles.length > 0" class="w-full flex flex-wrap gap-2 px-2 pt-2"> <div v-if="attachedFiles.length > 0" class="w-full flex flex-wrap gap-2 px-2 pt-2">
<div v-for="(file, index) in attachedFiles" :key="index" class="bg-gray-900 border border-gray-700 rounded px-2 py-1 text-xs text-gray-300 flex items-center gap-2"> <div v-for="(file, index) in attachedFiles" :key="index" class="bg-gray-900 border border-gray-700 rounded px-2 py-1 text-xs text-gray-300 flex items-center gap-2">
<i data-lucide="file" class="w-3.5 h-3.5 text-aethas-cyan"></i> <i data-lucide="file" class="w-3.5 h-3.5 text-aethas-cyan"></i>
@@ -457,9 +444,9 @@
const colPricing = ref(""); const colPricing = ref("");
const colDomain = ref(""); const colDomain = ref("");
const workers = ref(["gemini-3.5-flash-lite", "", "", "", ""]); const workers = ref(["", "", "", "", ""]);
const selectedPrompter = ref("gemini-3.5-flash-lite"); const selectedPrompter = ref("");
const selectedConcatenator = ref("gemini-3.5-flash-lite"); const selectedConcatenator = ref("");
const attachedFiles = ref([]); const attachedFiles = ref([]);
@@ -478,14 +465,26 @@
}); });
const getFilteredModelsForCol = (pricingFilter, domainFilter, currentSelectedId) => { const getFilteredModelsForCol = (pricingFilter, domainFilter, currentSelectedId) => {
return modelInfo.models.filter(m => { let filtered = modelInfo.models.filter(m => {
if (m.model_id === currentSelectedId) return true;
const matchDomain = domainFilter === "" || m.domain === domainFilter; const matchDomain = domainFilter === "" || m.domain === domainFilter;
let matchPrice = true; let matchPrice = true;
if (pricingFilter === 'free') matchPrice = m.is_free; if (pricingFilter === 'free') matchPrice = m.is_free;
if (pricingFilter === 'paid') matchPrice = !m.is_free; if (pricingFilter === 'paid') matchPrice = !m.is_free;
return matchDomain && matchPrice; return matchDomain && matchPrice;
}); });
if (currentSelectedId && currentSelectedId.trim() !== "") {
const exists = filtered.find(m => m.model_id === currentSelectedId);
if (!exists) {
const original = modelInfo.models.find(m => m.model_id === currentSelectedId);
if (original) {
filtered.unshift(original);
} else {
filtered.unshift({ model_id: currentSelectedId, name: currentSelectedId, domain: "Importé", pricing_prompt: 0, pricing_completion: 0 });
}
}
}
return filtered;
}; };
const formatDate = (isoString) => { const formatDate = (isoString) => {
@@ -546,7 +545,7 @@
}; };
const downloadTemplate = () => { const downloadTemplate = () => {
const templateContent = `# AETHAS38 - Configuration Multi-Agents\n\n**Rappel important :**\n- **Prompteur :** Le système de l'orchestrateur utilisera ce modèle pour traduire et optimiser votre requête initiale en ANGLAIS (langue native des IA).\n- **Concaténeur :** Le système utilisera ce modèle pour regrouper les réponses des travailleurs et traduire la synthèse finale en FRANÇAIS (en préservant le code brut).\n\n## Sélection des Modèles\nRenseignez l'ID exact du modèle (ex: gemini-3.5-flash-lite, mistralai/mistral-large-4-0). Laissez vide si inutilisé.\n\n- Prompteur : gemini-3.5-flash-lite\n- Travailleur 1 : \n- Travailleur 2 : \n- Travailleur 3 : \n- Travailleur 4 : \n- Travailleur 5 : \n- Concaténeur : gemini-3.5-flash-lite\n`; const templateContent = `# AETHAS38 - Configuration Multi-Agents\n\n**Rappel important :**\n- **Prompteur :** Le système de l'orchestrateur utilisera ce modèle pour traduire et optimiser votre requête initiale en ANGLAIS (langue native des IA).\n- **Concaténeur :** Le système utilisera ce modèle pour regrouper les réponses des travailleurs et traduire la synthèse finale en FRANÇAIS (en préservant le code brut).\n\n## Sélection des Modèles\nRenseignez l'ID exact du modèle (ex: gemini-3.5-flash-lite, mistralai/mistral-large-4-0). Laissez vide si inutilisé.\n\n- Prompteur : \n- Travailleur 1 : \n- Travailleur 2 : \n- Travailleur 3 : \n- Travailleur 4 : \n- Travailleur 5 : \n- Concaténeur : \n`;
downloadFile(templateContent, 'aethas38-config-agents.md'); downloadFile(templateContent, 'aethas38-config-agents.md');
}; };
@@ -556,21 +555,23 @@
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
const content = e.target.result; const content = e.target.result;
const extractModel = (key) => { const extractModel = (key) => {
const regex = new RegExp(`- ${key} : (.*)`, 'i'); const safeKey = key.replace(/[éèe]/gi, '[éèe]');
const regex = new RegExp(`-\\s*${safeKey}\\s*:\\s*([^\\r\\n]*)`, 'i');
const match = content.match(regex); const match = content.match(regex);
return match && match[1] ? match[1].trim() : ""; return match && match[1] ? match[1].trim() : "";
}; };
const checkAndSet = (val, defaultVal) => val !== "" ? val : defaultVal; const w1 = extractModel('Travailleur 1');
const w2 = extractModel('Travailleur 2');
const w3 = extractModel('Travailleur 3');
const w4 = extractModel('Travailleur 4');
const w5 = extractModel('Travailleur 5');
selectedPrompter.value = checkAndSet(extractModel('Prompteur'), 'gemini-3.5-flash-lite'); workers.value = [w1, w2, w3, w4, w5];
workers.value[0] = extractModel('Travailleur 1'); selectedPrompter.value = extractModel('Prompteur');
workers.value[1] = extractModel('Travailleur 2'); selectedConcatenator.value = extractModel('Concateneur');
workers.value[2] = extractModel('Travailleur 3');
workers.value[3] = extractModel('Travailleur 4');
workers.value[4] = extractModel('Travailleur 5');
selectedConcatenator.value = checkAndSet(extractModel('Concaténeur'), 'gemini-3.5-flash-lite');
alert("Configuration IA importée et appliquée avec succès !"); alert("Configuration IA importée et appliquée avec succès !");
}; };
@@ -623,10 +624,19 @@
const processFile = (file) => { const processFile = (file) => {
const reader = new FileReader(); const reader = new FileReader();
const textExts = ['.txt', '.md', '.csv', '.json', '.py', '.js', '.html', '.css', '.xml', '.yml', '.yaml', '.sh', '.bat', '.ps1', '.ini', '.cfg', '.conf', '.log'];
const isText = textExts.some(ext => file.name.toLowerCase().endsWith(ext)) || file.type.startsWith('text/');
reader.onload = (e) => { reader.onload = (e) => {
attachedFiles.value.push({ name: file.name, content: e.target.result }); attachedFiles.value.push({ name: file.name, content: e.target.result });
}; };
// Les fichiers binaires sont lus en Base64.
if (isText) {
reader.readAsText(file); reader.readAsText(file);
} else {
reader.readAsDataURL(file);
}
}; };
const removeFile = (index) => { attachedFiles.value.splice(index, 1); }; const removeFile = (index) => { attachedFiles.value.splice(index, 1); };
@@ -677,10 +687,20 @@
const data = await response.json(); const data = await response.json();
if (Array.isArray(data)) messages.value = data; if (Array.isArray(data)) messages.value = data;
await fetchFinances(); await fetchFinances();
} else if (response.status === 401) {
alert("Votre session a expiré. Veuillez vous reconnecter.");
window.location.href = '/login';
return;
} else { } else {
const err = await response.json(); alert("Erreur API IA : " + err.detail); const err = await response.json(); alert("Erreur API IA : " + err.detail);
} }
} catch (error) {} finally { isAiThinking.value = false; scrollToBottom(); nextTick(()=>lucide.createIcons()); } } catch (error) {
alert("Erreur locale lors de l'envoi : " + error.message);
} finally {
isAiThinking.value = false;
scrollToBottom();
nextTick(()=>lucide.createIcons());
}
}; };
const changePassword = async () => { const changePassword = async () => {
+3
View File
@@ -15,3 +15,6 @@ openai
httpx httpx
python-multipart python-multipart
pytz pytz
PyPDF2
openpyxl
oletools