import os import json import time import requests from datetime import datetime import tiktoken from modules.logger import log_event # FORCAGE DES CHEMINS ABSOLUS BASE_DIR = "/DATA/AppData/MULTI-IA-AETHAS38" os.makedirs(BASE_DIR, exist_ok=True) BALANCES_FILE = os.path.join(BASE_DIR, "aethas_balances.json") AUDIT_FILE = os.path.join(BASE_DIR, "aethas_financial_audit.json") TARIFS_FILE = os.path.join(BASE_DIR, "tarifs_openrouter.json") # VIP_ADMIN_USERS est conservé pour la compatibilité avec l'ancien code, # mais la logique stricte des rôles est maintenant dans la fonction d'estimation. VIP_ADMIN_USERS = ["xavier", "céline", "celine", "anaëlle", "anaelle", "renan", "audric"] # ----------------------------------------------------------------------------- # NOUVEAU MODULE : CONVERSION DE DEVISES (API FRANKFURTER) # ----------------------------------------------------------------------------- TAUX_DEVISE_FILE = os.path.join(BASE_DIR, "taux_devise.json") def obtenir_taux_usd_eur() -> float: """ Récupère le taux de change USD -> EUR via l'API Frankfurter. Met en cache le résultat pendant 12 heures pour ne pas ralentir le serveur. """ maintenant = time.time() # Vérification du cache (12 heures = 43200 secondes) if os.path.exists(TAUX_DEVISE_FILE): if maintenant - os.path.getmtime(TAUX_DEVISE_FILE) < 43200: try: with open(TAUX_DEVISE_FILE, "r", encoding="utf-8") as f: data = json.load(f) return float(data.get("taux", 0.92)) # 0.92 par défaut (sécurité) except Exception: pass try: # Appel à l'API publique (gratuite et sans clé) basée sur la BCE resp = requests.get("https://api.frankfurter.app/latest?from=USD&to=EUR", timeout=5) if resp.status_code == 200: taux = resp.json().get("rates", {}).get("EUR", 0.92) # Sauvegarde en cache with open(TAUX_DEVISE_FILE, "w", encoding="utf-8") as f: json.dump({"taux": taux, "timestamp": maintenant}, f) log_event("INFO", "BILLING", f"Taux USD->EUR mis à jour : {taux}") return taux except Exception as e: log_event("WARNING", "BILLING", f"Échec récupération du taux de change : {e}") return 0.92 # Taux de repli fixe en cas de panne réseau # ----------------------------------------------------------------------------- # GESTION DES TARIFS OPENROUTER # ----------------------------------------------------------------------------- def obtenir_tarifs_dynamiques() -> dict: maintenant = time.time() if os.path.exists(TARIFS_FILE): if maintenant - os.path.getmtime(TARIFS_FILE) < 7200: try: with open(TARIFS_FILE, "r", encoding="utf-8") as f: return json.load(f) except Exception: pass tarifs = {} try: resp = requests.get("https://openrouter.ai/api/v1/models", timeout=10) if resp.status_code == 200: data = resp.json().get("data", []) for m in data: pricing = m.get("pricing", {}) p_prompt = float(pricing.get("prompt", 0.0)) p_comp = float(pricing.get("completion", 0.0)) tarifs[m["id"]] = max(p_prompt, p_comp) with open(TARIFS_FILE, "w", encoding="utf-8") as f: json.dump(tarifs, f, indent=4) log_event("INFO", "BILLING", "Tarifs OpenRouter mis à jour dynamiquement.") return tarifs except Exception as e: log_event("ERROR", "BILLING", f"Échec récupération des tarifs : {e}") if os.path.exists(TARIFS_FILE): try: with open(TARIFS_FILE, "r", encoding="utf-8") as f: return json.load(f) except Exception: pass return {} def charger_soldes() -> dict: if os.path.exists(BALANCES_FILE): try: with open(BALANCES_FILE, "r", encoding="utf-8") as f: return json.load(f) except Exception: pass soldes = {"cagnotte_commune_vip": 35.00} sauvegarder_soldes(soldes) return soldes def sauvegarder_soldes(soldes: dict): with open(BALANCES_FILE, "w", encoding="utf-8") as f: json.dump(soldes, f, indent=4) def enregistrer_transaction_audit(utilisateur: str, rang: str, modele: str, tokens: int, cout_brut: float, prix_facture: float, type_op: str = "DEBIT_REQUETE"): # Conversion du coût final en Euros pour l'export comptable URSSAF taux_actuel = obtenir_taux_usd_eur() cout_brut_eur = cout_brut * taux_actuel prix_facture_eur = prix_facture * taux_actuel entree = { "timestamp": time.time(), "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "utilisateur": utilisateur, "rang": rang, "type_operation": type_op, "modele": modele, "tokens": tokens, "cout_brut_api": cout_brut, "prix_facture_client": prix_facture, # Données Euro pour le Livre de Recettes (Pandas) "cout_brut_api_eur": cout_brut_eur, "prix_facture_client_eur": prix_facture_eur } audits = [] if os.path.exists(AUDIT_FILE): try: with open(AUDIT_FILE, "r", encoding="utf-8") as f: audits = json.load(f) except Exception: audits = [] audits.append(entree) with open(AUDIT_FILE, "w", encoding="utf-8") as f: json.dump(audits, f, indent=4) def compter_tokens(texte: str, modele: str = "cl100k_base") -> int: try: enc = tiktoken.get_encoding(modele) return len(enc.encode(texte)) except Exception as e: log_event("ERROR", "BILLING", f"Erreur comptage tokens : {e}") return 0 # ----------------------------------------------------------------------------- # MOTEUR DE CALCUL DES MARGES COMMERCIALES (Règles strictes) # ----------------------------------------------------------------------------- def estimer_cout_facture(prompt: str, modele_id: str, rang_utilisateur: str = "standard", multi_ia: bool = False) -> tuple[float, float, int]: tokens = compter_tokens(prompt) tarifs = obtenir_tarifs_dynamiques() cout_unitaire = tarifs.get(modele_id, 0.0) cout_brut = tokens * cout_unitaire if multi_ia and cout_brut > 0: cout_brut *= 2 if cout_brut == 0.0: return 0.0, 0.0, tokens rang = rang_utilisateur.lower() # 1. PAS DE MARGE (Super Admin, Admin, Famille) if rang in ["super admin", "admin", "famille"]: prix_facture = cout_brut # 2. MARGE RÉDUITE 7.5% (Utilisateurs VIP choisis par Admin) elif rang == "vip": prix_facture = cout_brut / 0.925 # 3. MARGE STANDARD 15% (Utilisateurs par défaut) else: prix_facture = cout_brut / 0.85 return round(cout_brut, 6), round(prix_facture, 6), tokens def verifier_et_valider_budget(prompt: str, modele_id: str, utilisateur_nom: str = "xavier", rang_utilisateur: str = "super admin", multi_ia: bool = False): cout_brut, prix_facture, tokens = estimer_cout_facture(prompt, modele_id, rang_utilisateur, multi_ia) if prix_facture == 0.0: return True, 0.0 soldes = charger_soldes() utilisateur_cle = utilisateur_nom.lower() # Les rôles protégés tapent dans la cagnotte globale (carte bancaire de l'admin) if utilisateur_cle in VIP_ADMIN_USERS or rang_utilisateur.lower() in ["super admin", "admin", "famille"]: solde_disponible = soldes.get("cagnotte_commune_vip", 0.0) cle_compte = "cagnotte_commune_vip" # Les utilisateurs payants (VIP / Standard) utilisent leur propre recharge else: solde_disponible = soldes.get(utilisateur_cle, 0.0) cle_compte = utilisateur_cle if solde_disponible < prix_facture: log_event("WARNING", "BILLING", f"Rejet : solde insuffisant pour {utilisateur_nom} ({solde_disponible} $ < {prix_facture} $)") raise Exception(f"SOLDE_INSUFFISANT|{prix_facture}|{solde_disponible}") soldes[cle_compte] = round(solde_disponible - prix_facture, 6) sauvegarder_soldes(soldes) enregistrer_transaction_audit(utilisateur_nom, rang_utilisateur, modele_id, tokens, cout_brut, prix_facture) log_event("INFO", "BILLING", f"Requête validée [{rang_utilisateur}] - Débit : {prix_facture} $ - Solde restant : {soldes[cle_compte]} $") return True, prix_facture def obtenir_solde(utilisateur_nom: str) -> float: # --- SYNCHRONISATION DIRECTE POUR L'ADMINISTRATEUR PRINCIPAL --- if utilisateur_nom.lower() in ["xavier"]: api_key = os.getenv("OPENROUTER_API_KEY") if not api_key: log_event("CRITICAL", "BILLING", "Clé OPENROUTER_API_KEY introuvable dans l'environnement ! La synchronisation est impossible.") else: try: resp = requests.get("https://openrouter.ai/api/v1/auth/key", headers={"Authorization": f"Bearer {api_key}"}, timeout=5) if resp.status_code == 200: data = resp.json().get("data", {}) usage = float(data.get('usage', 0.0)) limit = data.get('limit') balance = data.get('balance') vrai_solde = 0.0 if limit is not None: vrai_solde = float(limit) - usage elif balance is not None: vrai_solde = float(balance) if vrai_solde > 0 or (limit is not None) or (balance is not None): soldes = charger_soldes() soldes["cagnotte_commune_vip"] = vrai_solde soldes["xavier"] = vrai_solde sauvegarder_soldes(soldes) return vrai_solde else: log_event("ERROR", "BILLING", f"Refus de l'API OpenRouter (Code {resp.status_code}) : {resp.text}") except Exception as e: log_event("WARNING", "BILLING", f"Échec réseau lors de la connexion à OpenRouter : {e}") # --- COMPORTEMENT STANDARD POUR LES AUTRES UTILISATEURS --- soldes = charger_soldes() if utilisateur_nom.lower() in VIP_ADMIN_USERS: return soldes.get("cagnotte_commune_vip", 0.0) return soldes.get(utilisateur_nom.lower(), 0.0) def generer_dossier_litige(utilisateur_nom: str, est_admin: bool = False) -> list: if not os.path.exists(AUDIT_FILE): return [] try: with open(AUDIT_FILE, "r", encoding="utf-8") as f: tous_les_audits = json.load(f) except Exception: return [] dossier = [] for item in tous_les_audits: if item.get("utilisateur", "").lower() == utilisateur_nom.lower(): if est_admin: dossier.append(item) else: dossier.append({ "date": item.get("date"), "type_operation": item.get("type_operation"), "modele": item.get("modele"), "tokens": item.get("tokens"), "montant_facture_ttc": item.get("prix_facture_client") }) return dossier