diff --git a/backend/main.py b/backend/main.py index c3c9b20..e68557e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -144,8 +144,11 @@ 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.file_content and message.file_name: - final_content = f"[Fichier attaché : {message.file_name}]\n```\n{message.file_content}\n```\n\n{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 db.add(Message(role=message.role, content=final_content, project_id=project_id)) db.commit() diff --git a/backend/orchestrator.py b/backend/orchestrator.py index 78f26f8..734a7e2 100644 --- a/backend/orchestrator.py +++ b/backend/orchestrator.py @@ -15,9 +15,7 @@ def determine_domain(model_id: str) -> str: return "Texte Polyvalent" async def sync_finances(db: Session, settings: SystemSettings): - """Interroge les fournisseurs pour récupérer le solde financier exact.""" async with httpx.AsyncClient(timeout=30.0) as client: - # OpenRouter if settings.openrouter_management_key or settings.openrouter_api_key: try: key = settings.openrouter_management_key or settings.openrouter_api_key @@ -38,10 +36,7 @@ async def sync_finances(db: Session, settings: SystemSettings): update_finance_db(db, "OpenRouter", balance, usage) except Exception as e: print(f"Erreur Finance OR: {e}") - # Groq if settings.groq_api_key: update_finance_db(db, "Groq", 999.0, 0.0) - - # DeepSeek if settings.deepseek_api_key: try: resp = await client.get("https://api.deepseek.com/user/balance", headers={"Authorization": f"Bearer {settings.deepseek_api_key}"}) @@ -49,11 +44,7 @@ async def sync_finances(db: Session, settings: SystemSettings): infos = resp.json().get("balance_infos", [{}])[0] update_finance_db(db, "DeepSeek", float(infos.get("total_balance", 0)), 0.0) except Exception: pass - - # Mistral AI if settings.mistral_api_key: update_finance_db(db, "Mistral", 0.0, 0.0) - - # Gemini if settings.gemini_api_key: update_finance_db(db, "Gemini", 0.0, 0.0) try: db.commit() @@ -70,11 +61,8 @@ def update_finance_db(db, provider, balance, usage): async def sync_providers_models(db: Session, settings: SystemSettings, sync_type: str = "Automatique"): added = 0 - # On stocke les modèles en mémoire avant de les envoyer en base pour éviter les doublons models_to_process = {} - async with httpx.AsyncClient(timeout=90.0) as client: - # 1. OpenRouter if settings.openrouter_api_key: try: resp = await client.get("https://openrouter.ai/api/v1/models") @@ -84,80 +72,62 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type m_id = item.get("id") if not m_id: continue pricing = item.get("pricing") or {} - try: pp = float(pricing.get("prompt") or 0.0) * 1000000 - except: pp = 0.0 - try: pc = float(pricing.get("completion") or 0.0) * 1000000 - except: pc = 0.0 + 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} except: pass except Exception as e: print(f"Erreur OR Models: {e}") - # 2. Groq if settings.groq_api_key: try: resp = await client.get("https://api.groq.com/openai/v1/models", headers={"Authorization": f"Bearer {settings.groq_api_key}"}) if resp.status_code == 200: for item in resp.json().get("data", []): - try: - 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} - except: pass + 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} except Exception: pass - # 3. DeepSeek if settings.deepseek_api_key: try: resp = await client.get("https://api.deepseek.com/models", headers={"Authorization": f"Bearer {settings.deepseek_api_key}"}) if resp.status_code == 200: for item in resp.json().get("data", []): - try: - 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} - except: pass + 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} except Exception: pass - # 4. Mistral if settings.mistral_api_key: try: resp = await client.get("https://api.mistral.ai/v1/models", headers={"Authorization": f"Bearer {settings.mistral_api_key}"}) if resp.status_code == 200: for item in resp.json().get("data", []): - try: - 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} - except: pass + 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} except Exception: pass - # 5. Gemini (Google) if settings.gemini_api_key: try: resp = await client.get(f"https://generativelanguage.googleapis.com/v1beta/models?key={settings.gemini_api_key}") if resp.status_code == 200: for item in resp.json().get("models", []): - try: - 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} - except: pass + 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} except Exception: pass - # 6. Cloudflare if settings.cloudflare_account_id and settings.cloudflare_api_token: try: url = f"https://api.cloudflare.com/client/v4/accounts/{settings.cloudflare_account_id}/ai/models/search" resp = await client.get(url, headers={"Authorization": f"Bearer {settings.cloudflare_api_token}"}) if resp.status_code == 200: for item in resp.json().get("result", []): - try: - 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} - except: pass + 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} except Exception: pass - # --- Phase d'enregistrement sécurisée --- for m_id, data in models_to_process.items(): try: existing = db.query(AIModel).filter(AIModel.model_id == m_id).first() @@ -166,13 +136,9 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type 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 - # Commit très fréquent pour éviter les gros blocs qui plantent - if added % 50 == 0: - db.commit() - except IntegrityError: - db.rollback() # Si conflit, on annule cette insertion et on continue - except Exception: - db.rollback() + if added % 50 == 0: db.commit() + except IntegrityError: db.rollback() + except Exception: db.rollback() try: settings.last_sync_date = datetime.now(timezone.utc) @@ -180,13 +146,11 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type db.commit() except Exception as e: db.rollback() - print(f"Erreur DB Commit Sync: {e}") await sync_finances(db, settings) return {"status": "success", "models_processed": added} def get_client_for_model(db: Session, model_id: str, settings: SystemSettings): - """Récupère dynamiquement le bon client OpenAI en fonction du fournisseur du modèle.""" model_db = db.query(AIModel).filter(AIModel.model_id == model_id).first() provider = model_db.provider if model_db else "openrouter" @@ -217,26 +181,39 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings, 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_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." + optimized = await ask_agent(p_client, p_mod, [{"role": "system", "content": prompt_system}, {"role": "user", "content": user_prompt}], p_prov) + if len(workers) == 1: w_mod = workers[0] client, provider = get_client_for_model(db, w_mod, settings) - final_response = await ask_agent(client, w_mod, formatted_history + [{"role": "user", "content": user_prompt}], provider) + worker_response = await ask_agent(client, w_mod, formatted_history + [{"role": "user", "content": optimized}], provider) + responses = [worker_response] else: - p_mod = config.get("prompter", "gemini-3.5-flash-lite") - p_client, p_prov = get_client_for_model(db, p_mod, settings) - optimized = await ask_agent(p_client, p_mod, [{"role": "system", "content": "Optimise cette requête."}, {"role": "user", "content": user_prompt}], p_prov) - w_tasks = [] for w in workers: w_client, w_prov = get_client_for_model(db, w, settings) 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) - c_mod = config.get("concatenator", "gemini-3.5-flash-lite") - c_client, c_prov = get_client_for_model(db, c_mod, settings) - synth = f"Requête: {user_prompt}\n\n" + "\n".join([f"--- EXPERT {i+1} ---\n{r}" for i, r in enumerate(responses)]) + "\n\nFais une synthèse finale." - final_response = await ask_agent(c_client, c_mod, [{"role": "user", "content": synth}], c_prov) + # É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_client, c_prov = get_client_for_model(db, c_mod, settings) + + concat_system = ( + "You are a master lead developer and technical synthesizer. " + "Synthesize the provided expert responses into a single cohesive response. " + "Translate all explanatory text, descriptions, and user-facing prose into natural French. " + "CRITICAL: Do NOT translate code blocks, programming keywords, or source code contents. " + "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)]) + final_response = await ask_agent(c_client, c_mod, [{"role": "system", "content": concat_system}, {"role": "user", "content": synth}], c_prov) + except Exception as e: final_response = f"L'IA a rencontré une erreur critique: {str(e)}" diff --git a/backend/schemas.py b/backend/schemas.py index 3ee312a..5b9339b 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -44,14 +44,17 @@ class OrchestratorConfig(BaseModel): prompter: Optional[str] = "gemini-3.5-flash-lite" concatenator: Optional[str] = "gemini-3.5-flash-lite" +class AttachedFile(BaseModel): + name: str + content: str + class MessageBase(BaseModel): role: str content: str class MessageCreate(MessageBase): config: Optional[OrchestratorConfig] = None - file_content: Optional[str] = None - file_name: Optional[str] = None + files: Optional[List[AttachedFile]] = None class MessageResponse(MessageBase): id: int diff --git a/frontend/dashboard.html b/frontend/dashboard.html index 09cf4bc..8998336 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -383,19 +383,28 @@ - -