Compare commits

..
4 Commits
Author SHA1 Message Date
xavier 44bf0b868e feat: intégration du parsing backend (PDF, Excel, VBA) et gestion sécurisée des champs prompteur/concaténeur vides dans les templates
Build and Push Docker Image / build-and-push (push) Successful in 41s
2026-10-08 10:25:02 +02:00
xavier c779c51a03 fix: renforcement drastique de la regex du parseur .md et injection automatique de modèle de secours pour empêcher les cases vides
Build and Push Docker Image / build-and-push (push) Successful in 47s
2026-10-08 09:52:53 +02:00
xavier eaa0ff0564 feat: ajout de l'import de configuration IA via fichier .md et bouton de téléchargement du template
Build and Push Docker Image / build-and-push (push) Successful in 44s
2026-10-08 09:35:13 +02:00
xavier ecd9854527 feat: passage des prompt en traduction anglaise. Mise en place du téléchargement d'un *.md des modèles. Traduction de la description des modèles en Français.
Build and Push Docker Image / build-and-push (push) Successful in 43s
2026-10-08 09:24:04 +02:00
4 changed files with 212 additions and 50 deletions

No files matched your search

+54 -2
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
@@ -147,7 +147,59 @@ async def create_message(project_id: int, message: MessageCreate, db: Session =
if message.files: if message.files:
files_text = "" files_text = ""
for f in message.files: for f in message.files:
files_text += f"\n\n[Fichier attaché : {f.name}]\n```\n{f.content}\n```" 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"
# Extraction des Macros VBA
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 = f"[Fichier binaire non supporté textuellement : {f.name}]"
files_text += f"\n\n[Fichier attaché : {f.name}]\n```text\n{extracted_text}\n```"
except Exception as e:
files_text += f"\n\n[Fichier attaché : {f.name} - ERREUR DE DECODAGE: {str(e)}]"
else:
files_text += f"\n\n[Fichier attaché : {f.name}]\n```\n{content}\n```"
final_content = message.content + files_text final_content = message.content + files_text
db.add(Message(role=message.role, content=final_content, project_id=project_id)) db.add(Message(role=message.role, content=final_content, project_id=project_id))
+42 -16
View File
@@ -1,5 +1,6 @@
import asyncio import asyncio
import httpx import httpx
import urllib.parse
from openai import AsyncOpenAI from openai import AsyncOpenAI
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy.orm import Session 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" if "audio" in mid or "whisper" in mid: return "Audio"
return "Texte Polyvalent" 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 def sync_finances(db: Session, settings: SystemSettings):
async with httpx.AsyncClient(timeout=30.0) as client: async with httpx.AsyncClient(timeout=30.0) as client:
if settings.openrouter_management_key or settings.openrouter_api_key: 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 pp = float(pricing.get("prompt") or 0.0) * 1000000
pc = float(pricing.get("completion") or 0.0) * 1000000 pc = float(pricing.get("completion") or 0.0) * 1000000
is_free = (pp == 0.0 and pc == 0.0) is_free = (pp == 0.0 and pc == 0.0)
desc = item.get("description", "Modèle OpenRouter.")[:200] + "..." desc_en = item.get("description", "Generic AI Model.")
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} 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: pass
except Exception as e: print(f"Erreur OR Models: {e}") 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: if resp.status_code == 200:
for item in resp.json().get("data", []): for item in resp.json().get("data", []):
m_id = item["id"] 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 except Exception: pass
if settings.deepseek_api_key: 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: if resp.status_code == 200:
for item in resp.json().get("data", []): for item in resp.json().get("data", []):
m_id = item["id"] 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 except Exception: pass
if settings.mistral_api_key: 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: if resp.status_code == 200:
for item in resp.json().get("data", []): for item in resp.json().get("data", []):
m_id = item["id"] 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 except Exception: pass
if settings.gemini_api_key: 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: if resp.status_code == 200:
for item in resp.json().get("models", []): for item in resp.json().get("models", []):
m_id = item["name"].replace("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": "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}
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}
except Exception: pass except Exception: pass
if settings.cloudflare_account_id and settings.cloudflare_api_token: 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: if resp.status_code == 200:
for item in resp.json().get("result", []): for item in resp.json().get("result", []):
m_id = item.get("name") 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": "Modèle Serverless Cloudflare Workers AI.", "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": desc, "domain": determine_domain(m_id), "is_free": True, "ctx": 4096, "pp": 0.0, "pc": 0.0}
except Exception: pass 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: try:
existing = db.query(AIModel).filter(AIModel.model_id == m_id).first() existing = db.query(AIModel).filter(AIModel.model_id == m_id).first()
if existing: 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: 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"])) 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 added += 1
@@ -181,8 +204,10 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings,
final_response = "" final_response = ""
try: try:
# Étape 1 : Le prompteur traduit et optimise la requête en ANGLAIS pour les travailleurs # Fallback sécurisé en cas de champ vide
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." prompt_system = "You are an expert prompt engineer. Translate and optimize the user request into clear, precise English tailored for AI execution."
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)
@@ -199,8 +224,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)
# É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")
c_mod = config.get("concatenator", "gemini-3.5-flash-lite") 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 = (
@@ -211,7 +237,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: {user_prompt}\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:
+112 -31
View File
@@ -292,11 +292,20 @@
</div> </div>
</div> </div>
<!-- BARRE DE SÉLECTION DE STYLE TABULAIRE (9 COLONNES STRICTES) --> <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:p-6 w-full max-w-7xl mx-auto space-y-2 flex-shrink-0"> <div class="flex justify-between items-center mb-2">
<div class="grid grid-cols-9 border border-black bg-gray-900 rounded-lg overflow-hidden text-center text-xs font-bold shadow-lg"> <span class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Configuration des IA</span>
<div class="flex gap-2">
<!-- 1. PAYANT / GRATUIT --> <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>
<!-- 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"> <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">
@@ -306,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">
@@ -318,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">
@@ -327,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">
@@ -336,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">
@@ -345,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">
@@ -354,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">
@@ -363,30 +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 (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"> <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-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>
@@ -446,11 +444,11 @@
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([]); // Liste de max 10 fichiers const attachedFiles = ref([]);
const pinnedProjects = computed(() => projects.value.filter(p => p.is_pinned)); const pinnedProjects = computed(() => projects.value.filter(p => p.is_pinned));
const recentProjects = 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) => { 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) => {
@@ -501,7 +511,30 @@
if(res.ok) { if(res.ok) {
await fetchModelsInfo(); await fetchModelsInfo();
await fetchFinances(); 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 { } else {
let errText = await res.text(); let errText = await res.text();
try { const errObj = JSON.parse(errText); errText = errObj.detail || errText; } catch(e){} try { const errObj = JSON.parse(errText); errText = errObj.detail || errText; } catch(e){}
@@ -511,6 +544,41 @@
isSyncing.value = false; 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 uploadAvatar = async (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
@@ -556,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 });
}; };
reader.readAsText(file);
// Les fichiers binaires (PDF, Excel, etc.) sont lus en Base64 pour que Python s'en charge.
if (isText) {
reader.readAsText(file);
} else {
reader.readAsDataURL(file);
}
}; };
const removeFile = (index) => { attachedFiles.value.splice(index, 1); }; const removeFile = (index) => { attachedFiles.value.splice(index, 1); };
@@ -589,7 +666,6 @@
isAiThinking.value = true; isAiThinking.value = true;
scrollToBottom(); scrollToBottom();
// Préparation du payload avec copie des fichiers avant destruction
const payloadFiles = [...attachedFiles.value]; const payloadFiles = [...attachedFiles.value];
const payload = { const payload = {
@@ -603,7 +679,6 @@
files: payloadFiles files: payloadFiles
}; };
// DESTRUCTION AUTOMATIQUE DES FICHIERS JOINTS APRÈS ENVOI
attachedFiles.value = []; attachedFiles.value = [];
try { try {
@@ -615,7 +690,13 @@
} 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 () => {
@@ -638,7 +719,7 @@
createNewProject, selectProject, renameProject, sendMessage, isLoading, togglePin, deleteProject, createNewProject, selectProject, renameProject, sendMessage, isLoading, togglePin, deleteProject,
isSettingsModalOpen, openSettings, closeSettings, passForm, settingsMessage, isSavingSettings, changePassword, uploadAvatar, isSettingsModalOpen, openSettings, closeSettings, passForm, settingsMessage, isSavingSettings, changePassword, uploadAvatar,
modelInfo, financesList, filterSearch, filterDomain, filterPricing, filteredModels, getFilteredModelsForCol, formatDate, syncModels, isSyncing, 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, attachedFiles, handleFileUpload, handlePaste, removeFile,
exportProject, copyToClipboard, downloadFile, downloadMessage, parseMessage exportProject, copyToClipboard, downloadFile, downloadMessage, parseMessage
}; };
+4 -1
View File
@@ -14,4 +14,7 @@ PyJWT
openai openai
httpx httpx
python-multipart python-multipart
pytz pytz
PyPDF2
openpyxl
oletools