Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83e70eadf6 | ||
|
|
60c0699c57 | ||
|
|
44bf0b868e | ||
|
|
c779c51a03 | ||
|
|
eaa0ff0564 | ||
|
|
ecd9854527 |
No files matched your search
+68
-10
@@ -3,7 +3,7 @@ from fastapi.responses import RedirectResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
import os, json, asyncio, shutil
|
||||
import os, json, asyncio, shutil, base64, io
|
||||
from datetime import datetime
|
||||
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()
|
||||
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.")
|
||||
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"}
|
||||
|
||||
@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])
|
||||
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
|
||||
if message.files:
|
||||
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
|
||||
extracted_files_data = []
|
||||
files_names = []
|
||||
|
||||
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()
|
||||
|
||||
history = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
|
||||
settings = db.query(SystemSettings).first()
|
||||
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.commit()
|
||||
|
||||
+90
-18
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import httpx
|
||||
import urllib.parse
|
||||
from openai import AsyncOpenAI
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -14,6 +15,19 @@ def determine_domain(model_id: str) -> str:
|
||||
if "audio" in mid or "whisper" in mid: return "Audio"
|
||||
return "Texte Polyvalent"
|
||||
|
||||
async def translate_en_to_fr(client: httpx.AsyncClient, text: str) -> str:
|
||||
if not text: return "Aucune description fournie."
|
||||
try:
|
||||
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)}"
|
||||
resp = await client.get(url, timeout=4.0)
|
||||
if resp.status_code == 200:
|
||||
translated = "".join([s[0] for s in resp.json()[0]])
|
||||
return translated + ("..." if len(text) > 300 else "")
|
||||
except Exception:
|
||||
pass
|
||||
return text[:200] + "..."
|
||||
|
||||
async def sync_finances(db: Session, settings: SystemSettings):
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
if settings.openrouter_management_key or settings.openrouter_api_key:
|
||||
@@ -75,8 +89,8 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type
|
||||
pp = float(pricing.get("prompt") or 0.0) * 1000000
|
||||
pc = float(pricing.get("completion") or 0.0) * 1000000
|
||||
is_free = (pp == 0.0 and pc == 0.0)
|
||||
desc = item.get("description", "Modèle OpenRouter.")[:200] + "..."
|
||||
models_to_process[m_id] = {"provider": "openrouter", "name": item.get("name", "Inconnu"), "desc": desc, "domain": determine_domain(m_id), "is_free": is_free, "ctx": item.get("context_length", 0), "pp": pp, "pc": pc}
|
||||
desc_en = item.get("description", "Generic AI Model.")
|
||||
models_to_process[m_id] = {"provider": "openrouter", "name": item.get("name", "Inconnu"), "desc_en": desc_en, "domain": determine_domain(m_id), "is_free": is_free, "ctx": item.get("context_length", 0), "pp": pp, "pc": pc}
|
||||
except: pass
|
||||
except Exception as e: print(f"Erreur OR Models: {e}")
|
||||
|
||||
@@ -86,7 +100,7 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type
|
||||
if resp.status_code == 200:
|
||||
for item in resp.json().get("data", []):
|
||||
m_id = item["id"]
|
||||
models_to_process[m_id] = {"provider": "groq", "name": m_id.capitalize(), "desc": "Modèle rapide LPU Groq.", "domain": determine_domain(m_id), "is_free": True, "ctx": 8192, "pp": 0.0, "pc": 0.0}
|
||||
models_to_process[m_id] = {"provider": "groq", "name": m_id.capitalize(), "desc": "Modèle très rapide hébergé sur LPU Groq.", "domain": determine_domain(m_id), "is_free": True, "ctx": 8192, "pp": 0.0, "pc": 0.0}
|
||||
except Exception: pass
|
||||
|
||||
if settings.deepseek_api_key:
|
||||
@@ -95,7 +109,7 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type
|
||||
if resp.status_code == 200:
|
||||
for item in resp.json().get("data", []):
|
||||
m_id = item["id"]
|
||||
models_to_process[m_id] = {"provider": "deepseek", "name": m_id.capitalize(), "desc": "Modèle officiel DeepSeek.", "domain": determine_domain(m_id), "is_free": False, "ctx": 64000, "pp": 0.14, "pc": 0.28}
|
||||
models_to_process[m_id] = {"provider": "deepseek", "name": m_id.capitalize(), "desc": "Modèle officiel du fournisseur DeepSeek.", "domain": determine_domain(m_id), "is_free": False, "ctx": 64000, "pp": 0.14, "pc": 0.28}
|
||||
except Exception: pass
|
||||
|
||||
if settings.mistral_api_key:
|
||||
@@ -104,7 +118,7 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type
|
||||
if resp.status_code == 200:
|
||||
for item in resp.json().get("data", []):
|
||||
m_id = item["id"]
|
||||
models_to_process[m_id] = {"provider": "mistral", "name": m_id.capitalize(), "desc": "Modèle officiel Mistral AI.", "domain": determine_domain(m_id), "is_free": False, "ctx": 32000, "pp": 0.2, "pc": 0.6}
|
||||
models_to_process[m_id] = {"provider": "mistral", "name": m_id.capitalize(), "desc": "Modèle officiel développé par Mistral AI.", "domain": determine_domain(m_id), "is_free": False, "ctx": 32000, "pp": 0.2, "pc": 0.6}
|
||||
except Exception: pass
|
||||
|
||||
if settings.gemini_api_key:
|
||||
@@ -113,8 +127,7 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type
|
||||
if resp.status_code == 200:
|
||||
for item in resp.json().get("models", []):
|
||||
m_id = item["name"].replace("models/", "")
|
||||
desc = item.get("description", "Modèle Google Gemini.")[:200] + "..."
|
||||
models_to_process[m_id] = {"provider": "gemini", "name": item.get("displayName", m_id), "desc": desc, "domain": determine_domain(m_id), "is_free": True, "ctx": item.get("inputTokenLimit", 32000), "pp": 0.0, "pc": 0.0}
|
||||
models_to_process[m_id] = {"provider": "gemini", "name": item.get("displayName", m_id), "desc": "Modèle natif de l'écosystème Google Gemini.", "domain": determine_domain(m_id), "is_free": True, "ctx": item.get("inputTokenLimit", 32000), "pp": 0.0, "pc": 0.0}
|
||||
except Exception: pass
|
||||
|
||||
if settings.cloudflare_account_id and settings.cloudflare_api_token:
|
||||
@@ -124,15 +137,25 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type
|
||||
if resp.status_code == 200:
|
||||
for item in resp.json().get("result", []):
|
||||
m_id = item.get("name")
|
||||
desc = item.get("description", "Modèle Cloudflare Workers AI.")[:200] + "..."
|
||||
models_to_process[m_id] = {"provider": "cloudflare", "name": m_id.split("/")[-1], "desc": desc, "domain": determine_domain(m_id), "is_free": True, "ctx": 4096, "pp": 0.0, "pc": 0.0}
|
||||
models_to_process[m_id] = {"provider": "cloudflare", "name": m_id.split("/")[-1], "desc": "Modèle Serverless Cloudflare Workers AI.", "domain": determine_domain(m_id), "is_free": True, "ctx": 4096, "pp": 0.0, "pc": 0.0}
|
||||
except Exception: pass
|
||||
|
||||
for m_id, data in models_to_process.items():
|
||||
sem = asyncio.Semaphore(15)
|
||||
async def process_and_translate(m_id, data, client_session):
|
||||
async with sem:
|
||||
if "desc_en" in data:
|
||||
data["desc"] = await translate_en_to_fr(client_session, data["desc_en"])
|
||||
return m_id, data
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client_trans:
|
||||
tasks = [process_and_translate(m_id, data, client_trans) for m_id, data in models_to_process.items()]
|
||||
translated_results = await asyncio.gather(*tasks)
|
||||
|
||||
for m_id, data in translated_results:
|
||||
try:
|
||||
existing = db.query(AIModel).filter(AIModel.model_id == m_id).first()
|
||||
if existing:
|
||||
existing.pricing_prompt = data["pp"]; existing.pricing_completion = data["pc"]; existing.is_free = data["is_free"]; existing.last_updated = datetime.now(timezone.utc)
|
||||
existing.pricing_prompt = data["pp"]; existing.pricing_completion = data["pc"]; existing.is_free = data["is_free"]; existing.description_fr = data["desc"]; existing.last_updated = datetime.now(timezone.utc)
|
||||
else:
|
||||
db.add(AIModel(provider=data["provider"], model_id=m_id, name=data["name"], description_fr=data["desc"], domain=data["domain"], is_free=data["is_free"], context_length=data["ctx"], pricing_prompt=data["pp"], pricing_completion=data["pc"]))
|
||||
added += 1
|
||||
@@ -171,20 +194,68 @@ async def ask_agent(client, model_id, messages, provider="openrouter"):
|
||||
kwargs = {"model": model_id, "messages": messages}
|
||||
if provider == "openrouter":
|
||||
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)
|
||||
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"])
|
||||
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]]
|
||||
|
||||
final_response = ""
|
||||
try:
|
||||
# Étape 1 : Le prompteur traduit et optimise la requête en ANGLAIS pour les travailleurs
|
||||
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)
|
||||
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)
|
||||
|
||||
if len(workers) == 1:
|
||||
@@ -199,8 +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))
|
||||
responses = await asyncio.gather(*w_tasks, return_exceptions=True)
|
||||
|
||||
# Étape 2 : Le concaténeur synthétise et traduit le retour en FRANÇAIS (en préservant le code et les commentaires)
|
||||
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)
|
||||
|
||||
concat_system = (
|
||||
@@ -211,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."
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
+114
-29
@@ -292,11 +292,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BARRE DE SÉLECTION DE STYLE TABULAIRE (9 COLONNES STRICTES) -->
|
||||
<div class="p-4 md:p-6 w-full max-w-7xl mx-auto space-y-2 flex-shrink-0">
|
||||
<div class="grid grid-cols-9 border border-black bg-gray-900 rounded-lg overflow-hidden text-center text-xs font-bold shadow-lg">
|
||||
<div class="p-4 md:px-6 md:pt-6 pb-2 w-full max-w-7xl mx-auto flex-shrink-0">
|
||||
<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>
|
||||
<div class="flex gap-2">
|
||||
<button @click="downloadTemplate" class="text-xs bg-gray-800 hover:bg-gray-700 text-aethas-cyan border border-gray-600 px-2 py-1 rounded flex items-center gap-1 transition-colors" title="Télécharger le modèle .md"><i data-lucide="download" class="w-3 h-3"></i> Template MD</button>
|
||||
<label class="text-xs bg-gray-800 hover:bg-gray-700 text-aethas-cyan border border-gray-600 px-2 py-1 rounded flex items-center gap-1 transition-colors cursor-pointer" title="Importer une configuration .md">
|
||||
<i data-lucide="upload" class="w-3 h-3"></i> Importer
|
||||
<input type="file" accept=".md" @change="handleConfigImport" class="hidden">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. PAYANT / GRATUIT -->
|
||||
<!-- 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="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>
|
||||
<select v-model="colPricing" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none">
|
||||
@@ -306,7 +315,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 2. TYPE (DOMAINE) -->
|
||||
<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>
|
||||
<select v-model="colDomain" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none">
|
||||
@@ -318,7 +326,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 3. TRAVAILLEUR 1 -->
|
||||
<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>
|
||||
<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">
|
||||
@@ -327,7 +334,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 4. TRAVAILLEUR 2 -->
|
||||
<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>
|
||||
<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">
|
||||
@@ -336,7 +342,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 5. TRAVAILLEUR 3 -->
|
||||
<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>
|
||||
<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">
|
||||
@@ -345,7 +350,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 6. TRAVAILLEUR 4 -->
|
||||
<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>
|
||||
<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">
|
||||
@@ -354,7 +358,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 7. TRAVAILLEUR 5 -->
|
||||
<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>
|
||||
<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">
|
||||
@@ -363,30 +366,25 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 8. PROMPTEUR -->
|
||||
<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>
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 9. CONCATENEUR -->
|
||||
<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>
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Champ de texte et boutons d'envoi / multi-fichiers (jusqu'à 10) -->
|
||||
<!-- 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">
|
||||
|
||||
<!-- Liste des fichiers attachés (max 10) -->
|
||||
<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">
|
||||
<i data-lucide="file" class="w-3.5 h-3.5 text-aethas-cyan"></i>
|
||||
@@ -446,11 +444,11 @@
|
||||
const colPricing = ref("");
|
||||
const colDomain = ref("");
|
||||
|
||||
const workers = ref(["gemini-3.5-flash-lite", "", "", "", ""]);
|
||||
const selectedPrompter = ref("gemini-3.5-flash-lite");
|
||||
const selectedConcatenator = ref("gemini-3.5-flash-lite");
|
||||
const workers = ref(["", "", "", "", ""]);
|
||||
const selectedPrompter = ref("");
|
||||
const selectedConcatenator = ref("");
|
||||
|
||||
const attachedFiles = ref([]); // Liste de max 10 fichiers
|
||||
const attachedFiles = ref([]);
|
||||
|
||||
const pinnedProjects = computed(() => projects.value.filter(p => p.is_pinned));
|
||||
const recentProjects = computed(() => projects.value.filter(p => !p.is_pinned));
|
||||
@@ -467,14 +465,26 @@
|
||||
});
|
||||
|
||||
const getFilteredModelsForCol = (pricingFilter, domainFilter, currentSelectedId) => {
|
||||
return modelInfo.models.filter(m => {
|
||||
if (m.model_id === currentSelectedId) return true;
|
||||
let filtered = modelInfo.models.filter(m => {
|
||||
const matchDomain = domainFilter === "" || m.domain === domainFilter;
|
||||
let matchPrice = true;
|
||||
if (pricingFilter === 'free') matchPrice = m.is_free;
|
||||
if (pricingFilter === 'paid') matchPrice = !m.is_free;
|
||||
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) => {
|
||||
@@ -501,7 +511,30 @@
|
||||
if(res.ok) {
|
||||
await fetchModelsInfo();
|
||||
await fetchFinances();
|
||||
alert("Synchronisation réussie !");
|
||||
|
||||
let mdContent = `# Extraction des Modèles IA - AETHAS38\n\n`;
|
||||
const d = new Date();
|
||||
const dateStr = d.getFullYear() + String(d.getMonth()+1).padStart(2, '0') + String(d.getDate()).padStart(2, '0');
|
||||
mdContent += `**Date d'extraction :** ${d.toLocaleDateString('fr-FR')} à ${d.toLocaleTimeString('fr-FR')}\n\n`;
|
||||
|
||||
const providers = [...new Set(modelInfo.models.map(m => m.provider))];
|
||||
providers.forEach(prov => {
|
||||
mdContent += `## Fournisseur : ${prov.toUpperCase()}\n\n`;
|
||||
const provModels = modelInfo.models.filter(m => m.provider === prov);
|
||||
provModels.forEach(m => {
|
||||
const priceInfo = m.is_free ? "**GRATUIT**" : `In: $${m.pricing_prompt.toFixed(2)} / Out: $${m.pricing_completion.toFixed(2)}`;
|
||||
const ctxInfo = `${(m.context_length / 1000).toFixed(0)}k`;
|
||||
mdContent += `- **${m.name || m.model_id}** (\`${m.model_id}\`)\n`;
|
||||
mdContent += ` - *Domaine :* ${m.domain}\n`;
|
||||
mdContent += ` - *Prix (1M tokens) :* ${priceInfo}\n`;
|
||||
mdContent += ` - *Contexte :* ${ctxInfo}\n`;
|
||||
mdContent += ` - *Description :* ${m.description_fr.replace(/\n/g, ' ')}\n\n`;
|
||||
});
|
||||
});
|
||||
|
||||
const filename = `${dateStr}-extraction-modèles.md`;
|
||||
downloadFile(mdContent, filename);
|
||||
alert("Synchronisation réussie ! Le fichier markdown a été généré et téléchargé.");
|
||||
} else {
|
||||
let errText = await res.text();
|
||||
try { const errObj = JSON.parse(errText); errText = errObj.detail || errText; } catch(e){}
|
||||
@@ -511,6 +544,41 @@
|
||||
isSyncing.value = false;
|
||||
};
|
||||
|
||||
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 : \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');
|
||||
};
|
||||
|
||||
const handleConfigImport = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const content = e.target.result;
|
||||
|
||||
const extractModel = (key) => {
|
||||
const safeKey = key.replace(/[éèe]/gi, '[éèe]');
|
||||
const regex = new RegExp(`-\\s*${safeKey}\\s*:\\s*([^\\r\\n]*)`, 'i');
|
||||
const match = content.match(regex);
|
||||
return match && match[1] ? match[1].trim() : "";
|
||||
};
|
||||
|
||||
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');
|
||||
|
||||
workers.value = [w1, w2, w3, w4, w5];
|
||||
selectedPrompter.value = extractModel('Prompteur');
|
||||
selectedConcatenator.value = extractModel('Concateneur');
|
||||
|
||||
alert("Configuration IA importée et appliquée avec succès !");
|
||||
};
|
||||
reader.readAsText(file);
|
||||
event.target.value = null;
|
||||
};
|
||||
|
||||
const uploadAvatar = async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
@@ -556,10 +624,19 @@
|
||||
|
||||
const processFile = (file) => {
|
||||
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) => {
|
||||
attachedFiles.value.push({ name: file.name, content: e.target.result });
|
||||
};
|
||||
|
||||
// Les fichiers binaires sont lus en Base64.
|
||||
if (isText) {
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const removeFile = (index) => { attachedFiles.value.splice(index, 1); };
|
||||
@@ -589,7 +666,6 @@
|
||||
isAiThinking.value = true;
|
||||
scrollToBottom();
|
||||
|
||||
// Préparation du payload avec copie des fichiers avant destruction
|
||||
const payloadFiles = [...attachedFiles.value];
|
||||
|
||||
const payload = {
|
||||
@@ -603,7 +679,6 @@
|
||||
files: payloadFiles
|
||||
};
|
||||
|
||||
// DESTRUCTION AUTOMATIQUE DES FICHIERS JOINTS APRÈS ENVOI
|
||||
attachedFiles.value = [];
|
||||
|
||||
try {
|
||||
@@ -612,10 +687,20 @@
|
||||
const data = await response.json();
|
||||
if (Array.isArray(data)) messages.value = data;
|
||||
await fetchFinances();
|
||||
} else if (response.status === 401) {
|
||||
alert("Votre session a expiré. Veuillez vous reconnecter.");
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
} else {
|
||||
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 () => {
|
||||
@@ -638,7 +723,7 @@
|
||||
createNewProject, selectProject, renameProject, sendMessage, isLoading, togglePin, deleteProject,
|
||||
isSettingsModalOpen, openSettings, closeSettings, passForm, settingsMessage, isSavingSettings, changePassword, uploadAvatar,
|
||||
modelInfo, financesList, filterSearch, filterDomain, filterPricing, filteredModels, getFilteredModelsForCol, formatDate, syncModels, isSyncing,
|
||||
workers, selectedPrompter, selectedConcatenator, colPricing, colDomain,
|
||||
workers, selectedPrompter, selectedConcatenator, colPricing, colDomain, downloadTemplate, handleConfigImport,
|
||||
attachedFiles, handleFileUpload, handlePaste, removeFile,
|
||||
exportProject, copyToClipboard, downloadFile, downloadMessage, parseMessage
|
||||
};
|
||||
|
||||
@@ -15,3 +15,6 @@ openai
|
||||
httpx
|
||||
python-multipart
|
||||
pytz
|
||||
PyPDF2
|
||||
openpyxl
|
||||
oletools
|
||||
Reference in new issue
Block a user