Compare commits

..
2 Commits
Author SHA1 Message Date
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
2 changed files with 111 additions and 24 deletions

No files matched your search

+35 -13
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,20 @@ 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:
"""Traduit automatiquement l'anglais vers le français via l'API publique Google Translate."""
if not text: return "Aucune description fournie."
try:
short_text = text[:300].strip() # Limite stricte pour l'API gratuite
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 +90,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 +101,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 +110,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 +119,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 +128,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 +138,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,7 +205,6 @@ 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
p_mod = config.get("prompter", "gemini-3.5-flash-lite") p_mod = config.get("prompter", "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."
@@ -199,7 +222,6 @@ 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", "gemini-3.5-flash-lite") c_mod = config.get("concatenator", "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)
+76 -11
View File
@@ -292,9 +292,21 @@
</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"> <!-- EN-TÊTE CONFIGURATION IA (AVEC BOUTONS IMPORT/EXPORT) -->
<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="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>
<!-- BARRE DE SÉLECTION DE STYLE TABULAIRE (9 COLONNES STRICTES) -->
<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 --> <!-- 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">
@@ -380,13 +392,12 @@
<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) --> <!-- Champ de texte et boutons d'envoi / multi-fichiers -->
<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) --> <!-- 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>
@@ -450,7 +461,7 @@
const selectedPrompter = ref("gemini-3.5-flash-lite"); const selectedPrompter = ref("gemini-3.5-flash-lite");
const selectedConcatenator = ref("gemini-3.5-flash-lite"); const selectedConcatenator = ref("gemini-3.5-flash-lite");
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));
@@ -501,7 +512,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 +545,39 @@
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 : 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`;
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 regex = new RegExp(`- ${key} : (.*)`, 'i');
const match = content.match(regex);
return match && match[1] ? match[1].trim() : "";
};
const checkAndSet = (val, defaultVal) => val !== "" ? val : defaultVal;
selectedPrompter.value = checkAndSet(extractModel('Prompteur'), 'gemini-3.5-flash-lite');
workers.value[0] = extractModel('Travailleur 1');
workers.value[1] = extractModel('Travailleur 2');
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 !");
};
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;
@@ -589,7 +656,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 +669,6 @@
files: payloadFiles files: payloadFiles
}; };
// DESTRUCTION AUTOMATIQUE DES FICHIERS JOINTS APRÈS ENVOI
attachedFiles.value = []; attachedFiles.value = [];
try { try {
@@ -638,7 +703,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
}; };