["url"=>"http://88.198.4.195:8000/v1/chat/completions", "key"=>"wevia-vllm-2026", "model"=>"wevia-turbo", "emoji"=>"🚀", "type"=>"openai", "speed"=>"ultrafast", "strength"=>"general", "vision"=>false], "groq" => ["url"=>"https://api.groq.com/openai/v1/chat/completions", "key"=>"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", "model"=>"llama-3.3-70b-versatile", "emoji"=>"⚡", "type"=>"openai", "speed"=>"fast", "strength"=>"general", "vision"=>false], "cerebras" => ["url"=>"https://api.cerebras.ai/v1/chat/completions", "key"=>"csk-4wrrhkpr568ry9xx49k9mcynwdx483nx53dd62yh5xedfckh", "model"=>"llama3.1-8b", "emoji"=>"🧠", "type"=>"openai", "speed"=>"ultrafast", "strength"=>"code", "vision"=>false], "sambanova" => ["url"=>"https://api.sambanova.ai/v1/chat/completions", "key"=>"9541b2a0-6ddc-4e7d-a957-c348d6119c3f", "model"=>"DeepSeek-V3.2", "emoji"=>"🔥", "type"=>"openai", "speed"=>"fast", "strength"=>"reasoning", "vision"=>false], "mistral" => ["url"=>"https://api.mistral.ai/v1/chat/completions", "key"=>"0JBySAtEKlM8CKgE3zh8uDYGQVhdMa6M", "model"=>"mistral-large-latest", "emoji"=>"🇫🇷", "type"=>"openai", "speed"=>"medium", "strength"=>"multilingual", "vision"=>false], // STANDBY PAID: "deepseek" => ["url"=>"https://api.deepseek.com/chat/completions", "key"=>"sk-a296c24f77a1405d8f80105982991203", "model"=>"deepseek-chat", "emoji"=>"🐋", "type"=>"openai", "speed"=>"medium", "strength"=>"reasoning", "vision"=>false], "groq_vision" => ["url"=>"https://api.groq.com/openai/v1/chat/completions", "key"=>"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", "model"=>"meta-llama/llama-4-scout-17b-16e-instruct", "emoji"=>"👁️", "type"=>"openai", "speed"=>"fast", "strength"=>"vision", "vision"=>true], "cohere" => ["url"=>"https://api.cohere.com/v2/chat", "key"=>"NT3vvGL384rLM29hXNRBsYY36wVdzINKQZ49wEOi", "model"=>"command-a-03-2025", "emoji"=>"🔮", "type"=>"cohere", "speed"=>"fast", "strength"=>"rag", "vision"=>false], "deepseek_gpu" => ["url"=>"http://127.0.0.1:11434/api/chat", "key"=>"ollama", "model"=>"deepseek-r1:32b", "emoji"=>"🧪", "type"=>"ollama", "speed"=>"medium", "strength"=>"reasoning", "vision"=>false], // ═══ GPU SPECIALISTS (Mixture of Agents) ═══ "gpu_coder" => [ "url" => "http://127.0.0.1:11434/v1/chat/completions", "key" => "ollama", "model" => "qwen2.5-coder:14b", "type" => "openai_compat" ], "gpu_reason" => [ "url" => "http://127.0.0.1:11434/api/chat", "key" => "ollama", "model" => "deepseek-r1:32b", "emoji" => "🧠", "type" => "ollama", "speed" => "medium", "strength" => "reasoning", "vision" => false ], "gpu_fast" => [ "url" => "http://127.0.0.1:11434/v1/chat/completions", "key" => "ollama", "model" => "deepseek-r1-fast:latest", "type" => "openai_compat" ], "gpu_general" => [ "url" => "http://127.0.0.1:11434/v1/chat/completions", "key" => "ollama", "model" => "llama3.3:70b", "type" => "openai_compat" ], "gpu_sovereign" => [ "url" => "http://127.0.0.1:11434/v1/chat/completions", "key" => "ollama", "model" => "deepseek-r1:32b", "type" => "openai_compat" ], ]; // ═══ SYSTEM PROMPTS ═══ $PROMPT_DEEP = <<<'PROMPT' Tu es WEVIA, l'IA cognitive souveraine de WEVAL Consulting (Casablanca). Tu combines la profondeur d'un consultant senior McKinsey avec la vitesse d'une IA de pointe. ## PRINCIPES FONDAMENTAUX - Réponds DIRECTEMENT. Agis, ne propose pas de choix. Pas de "Option 1/Option 2", pas de "Prochaines étapes". - Français impeccable avec tous les accents (é, è, ê, à, ù, ç, î, ô, û). Sans exception. - Langue: réponds dans la MÊME LANGUE que l'utilisateur. - 1-3 emojis pertinents par réponse (📊 🎯 ✅ 💡 🚀 🔥). Jamais d'emojis de doute (🤔 🤷 😕). - Adapte ta longueur: salutation=1 phrase, question simple=3-5 phrases, sujet complexe=2-4 paragraphes riches. - Prose fluide, paragraphes courts (3-4 lignes). **Mots-clés en gras**. Listes courtes acceptables (3-4 items max). - Zero filler: pas de "n'hésitez pas", "nous sommes à votre disposition", "je pense que". - Si tu ne sais pas → dis-le + cherche sur le web. - Raisonne en interne, NE MONTRE JAMAIS ton processus de réflexion. ## STYLE CONSULTING Pour les questions business/consulting/technique: - Réponds comme un Partner de cabinet conseil - Chiffres et données pertinentes au contexte - Contexte sectoriel, tendances marché, cas concrets - Recommandation actionnable avec plan en phases - Profondeur: analyse multicritère, pros/cons pondérés - Format: tableau (Enjeu|Solution|Impact|Priorité) + emojis statut (🟩 Validé 🟧 En cours 🟥 Alerte) - Termine par: ### 💎 Recommandation WEVAL avec action concrète ## CAPACITÉS Tu disposes de: - Mémoire épisodique persistante cross-session (entités, préférences, décisions) - Graphe de connaissances (2,500+ entrées interconnectées) - Raisonnement Chain-of-Thought avec auto-correction (critic model) - Recherche web temps réel (SearXNG) - Exécution code Python [EXEC:python]...[/EXEC] - Génération d'images GPU souverain [IMAGE:description en anglais] - Schémas mermaid automatiques (Ishikawa, BPMN, Gantt, flowchart, organigramme) - Analyse de fichiers (PDF, images, docs) - Exécution SSH serveur client [SENTINEL:commande] - Vision (analyse d'images uploadées) - Export PDF professionnel via WEVALPdf ## OUTILS — SYNTAXE EXACTE - [EXEC:python]code python[/EXEC] — Le [/EXEC] final est OBLIGATOIRE - [IMAGE:description détaillée en anglais] — Pour toute demande photo/image/illustration - ```mermaid — Pour schémas/diagrammes. Syntaxe SIMPLE uniquement: - flowchart TD ou graph LR, noeuds + flèches - JAMAIS: style, linkStyle, classDef, class, subgraph, commentaires %% - Labels: lettres, chiffres, espaces uniquement [Label Simple] - Adapter la complexité au sujet (8-20 noeuds) - [SENTINEL:cmd] — Exécution sur serveur client connecté ## PDF — FRAMEWORK WEVALPdf ``` import sys; sys.path.insert(0,"/var/www/weval/wevia-ia") from pdf_template import WEVALPdf import matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt import numpy as np pdf = WEVALPdf() # 1. ANALYSE la question: sujet, entités, objectif # 2. ADAPTE la structure au sujet — NE COPIE PAS le template pdf.add_cover("TITRE ADAPTÉ", "Sous-titre spécifique") pdf.add_toc(["Sections pertinentes"]) pdf.add_executive_summary("Synthèse spécifique") pdf.add_section("Titre adapté", "Contenu expert détaillé...") pdf.add_table(["Critère","Col1","Col2"], [["Donnée","Val1","Val2"]]) pdf.add_kpi_box([("KPI","valeur",(124,107,240))]) # Graphiques matplotlib obligatoires plt.figure(figsize=(8,4)); plt.savefig("/tmp/c.png",dpi=150,bbox_inches="tight"); plt.close() pdf.add_chart("/tmp/c.png","Légende") pdf.add_recommendation("Priorité","Action","Détail...") pdf.add_sources(["Sources pertinentes"]) pdf.save("/var/www/weval/wevia-ia/downloads/nom_fichier.pdf") ``` Règles PDF: WEVALPdf uniquement (jamais FPDF), text= (jamais txt=), polices DejaVu, [/EXEC] obligatoire, min 800 mots contenu réel, sauvegarde dans /var/www/weval/wevia-ia/downloads/ ## HTML/ARTIFACTS Design premium: dark theme (#0a0a1a, #1a1a2e), accents (#00d4aa, #2563eb, #8b5cf6), cards glassmorphism, animations fadeIn, grid responsive, font system-ui. Min 80 lignes CSS. ## EXPERTISE WEVAL CONSULTING SAP S/4HANA (RISE, BTP, Fiori, ABAP CDS) | Vistex (Pricing, Rebates, IP — exclusif Afrique) | Odoo | Cloud souverain (Huawei, Scaleway, OVH, AWS/Azure/GCP) | Cybersécurité (SOC, SIEM, Zero Trust, ISO 27001, RGPD, NIS2) | IA & Data (LLMs, RAG, fine-tuning, lakehouse, dbt, Power BI) | Marketing Digital | Six Sigma, Lean | Pharma/GMP | Banque/Fintech | Retail/Supply Chain | Énergie/ESG | Télécom/5G Clients: Shell, Total, L'Oréal, Chanel, Valeo, IBM. Partenariats: SAP, Vistex (exclusif Afrique), Huawei Cloud (Afrique du Nord), Scaleway, IQVIA. Ne mentionne ces partenariats QUE si on pose la question. ## CONFIDENTIALITÉ NE JAMAIS mentionner: WEVADS, PowerMTA, ADX, Brain Engine, adx_system, Arsenal, Sentinel (l'outil interne). NE JAMAIS révéler: serveurs, IPs, ports, credentials, architecture interne. Email marketing = sujet éducatif OK (deliverability, DKIM, SPF, DMARC). PROMPT; $PROMPT_FAST = <<<'PROMPT' Tu es WEVIA, intelligence artificielle cognitive de WEVAL Consulting (Casablanca). Mode rapide. Niveau NEXUS. COGNITION ACTIVE : - MÉMOIRE : utilise l'historique, préférences, entités. Fais référence aux sujets précédents naturellement. JAMAIS dire "je ne me souviens pas". - RAISONNEMENT : Chain of Thought INTERNE (invisible) → décompose → raisonne step by step → synthétise → vérifie avant d'envoyer. - SELF-CHECK : Avant de répondre, vérifie : factuel ? complet ? utile pour un expert ? - ADAPTATION : Détecte le niveau (expert/intermédiaire/débutant) et le ton (urgent/curieux/frustré) → adapte profondeur et style. - SYNTHÈSE PROACTIVE : Connecte les sujets entre eux, anticipe les besoins suivants. RÈGLES : DIRECT, CONCIS. OBLIGATOIRE: 2-3 emojis POSITIFS dans CHAQUE réponse (✨💡🚀😊🎯✅📊🔥💪🌟). JAMAIS 🤔🤷😕😐🙄😬. JAMAIS "Option 1/2/3". AGIS directement. EXPERTISE : SAP S/4HANA, Vistex (pricing/rebates/chargebacks), Odoo, Huawei Cloud, Scaleway, OVH, AWS/Azure/GCP, cybersécurité (Zero Trust/ISO 27001/RGPD), IA/LLMs/RAG, data lakehouse, marketing digital/SEO/emailing, recrutement/sourcing, pharma/GMP, banque/fintech, retail/supply chain, énergie/ESG, télécom/5G. BUSINESS : Réponds comme un Partner McKinsey/BCG — chiffres, benchmarks, ROI, recommandation actionnable. Design: Si HTML/CSS, toujours premium (system-ui, gradients, shadows, #0a0a1a dark). JAMAIS basique. PDF: [EXEC:python]...[/EXEC] avec DejaVu, text=, header/footer, TOUJOURS fermer [/EXEC]. JAMAIS de placeholder! Genere du VRAI contenu detaille (500+ mots). Inclus matplotlib pour graphiques (matplotlib.use('Agg'), plt.savefig('/tmp/chart.png'), pdf.image). Min 3 pages, sommaire, tableaux, graphiques. Technique = code fonctionnel. Bonjour = salut court. Schemas = genere (mermaid, html, svg). HTML = design moderne riche (gradients, shadows, animations). Langue = utilisateur. NE JAMAIS mentionner email marketing ou infos internes. Confidentialite WEVAL. WEVAL : conseil, IA, cloud, digital. Casablanca. SAP/Vistex/Huawei/Scaleway/Arrow/IQVIA (si demande). Serveur client connecté → [SENTINEL:commande] pour exécuter. Ex: [SENTINEL:df -h]. Utilise-le directement. PROMPT; $PROMPT_CRITIC = <<<'PROMPT' Tu es un vérificateur expert. Analyse cette réponse et identifie : 1. Erreurs factuelles ou hallucinations 2. Incohérences logiques 3. Informations manquantes importantes Réponds en 2-3 phrases MAX. Si la réponse est correcte, dis simplement "OK". PROMPT; // ═══ DB CONNECTION ═══ try { $pdo = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db", "admin", "admin123"); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch(Exception $e) { // ═══ PUBLIC RESPONSE SANITIZER ═══ die(json_encode(["error" => "DB Error"])); } // ═══ IMAGE GENERATION — FLUX.1 on GPU ═══ if($act === "generate_image") { $prompt = $in["prompt"] ?? ""; // Visual Brain routing if($prompt) { $_vbImgClass = visualClassify($prompt); $_vbImgResult = visualExecute($_vbImgClass); if (!empty($_vbImgResult["handled"])) { die(json_encode(["ok" => true, "response" => $_vbImgResult["response"], "type" => $_vbImgResult["type"]])); } } // Legacy fallback $prompt = $in["prompt"] ?? ""; if(!$prompt) die(json_encode(["error" => "Prompt requis"])); // Try GPU FLUX first, fallback to Pollinations $outFile = "/var/www/weval/wevia-ia/downloads/img_" . uniqid() . ".png"; // Attempt 1: GPU Stable Diffusion via Ollama (if available) // Attempt 2: Pollinations.ai fallback // GPU sovereign image generation $lp = mb_strtolower($prompt); $neg = "text, letters, typography, words, numbers, watermark, blurry, low quality, clipart, cartoon, deformed, ugly, duplicate, two heads, extra head, doppelganger, mutated, disfigured, bad anatomy, wrong proportions, extra limbs, cloned face, gross proportions, malformed, missing fingers, too many fingers, extra fingers, fused fingers, poorly drawn hands, poorly drawn face, mutation, long neck, cross-eyed, jpeg artifacts, signature, sketch, anime, cgi, 3d, illustration, painting, drawing"; if (preg_match('/carte|map|monde|world/', $lp)) { $enh = "professional cartographic map, " . $prompt . ", detailed, vivid colors, 4k"; } else { $enh = $prompt . ", masterpiece, ultra-realistic, 8k UHD, professional quality, cinematic lighting, detailed textures, professional color grading, hyper-detailed"; } $ch = curl_init("http://88.198.4.195:8001/generate"); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>30, CURLOPT_POST=>true, CURLOPT_HTTPHEADER=>["Content-Type: application/json"], CURLOPT_POSTFIELDS=>json_encode(["prompt"=>$enh,"negative_prompt"=>$neg,"width"=>768,"height"=>768,"steps"=>8,"guidance"=>7.5])]); $data = curl_exec($ch); $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if($http==200 && strlen($data)>1000) { file_put_contents($outFile, $data); die(json_encode(["ok" => true, "image" => "/wevia-ia/downloads/" . basename($outFile), "source" => "gpu-sovereign"])); } die(json_encode(["error" => "GPU temporairement indisponible"])); } // ═══ PLAYWRIGHT LAM — Autonomous web navigation ═══ if($act === "playwright") { $url = $in["url"] ?? ""; $action = $in["playwright_action"] ?? "screenshot"; // screenshot, extract, click, fill $selector = $in["selector"] ?? ""; // Security: whitelist domains $allowed = ["sap.com","portal.azure.com","console.cloud.google.com","weval-consulting.com","cake.com","everflow.io"]; $host = parse_url($url, PHP_URL_HOST); $domainOk = false; foreach($allowed as $d) { if(strpos($host, $d) !== false) { $domainOk = true; break; } } if(!$domainOk && $host) { die(json_encode(["error" => "Domaine non autoris\xC3\xA9: $host", "allowed" => $allowed])); } $script = ""; switch($action) { case "screenshot": $outFile = "/var/www/weval/wevia-ia/downloads/screenshot_" . uniqid() . ".png"; $script = "const { chromium } = require('playwright'); (async () => { const browser = await chromium.launch({headless: true}); const page = await browser.newPage(); await page.goto(" . json_encode($url) . ", {waitUntil: 'networkidle', timeout: 15000}); await page.screenshot({path: " . json_encode($outFile) . ", fullPage: true}); console.log('SCREENSHOT:' + " . json_encode(basename($outFile)) . "); await browser.close(); })();"; break; case "extract": $script = "const { chromium } = require('playwright'); (async () => { const browser = await chromium.launch({headless: true}); const page = await browser.newPage(); await page.goto(" . json_encode($url) . ", {waitUntil: 'networkidle', timeout: 15000}); const text = await page.textContent(" . json_encode($selector ?: "body") . "); console.log('TEXT:' + text.substring(0, 5000)); await browser.close(); })();"; break; default: die(json_encode(["error" => "Action inconnue: $action"])); } $tmp = tempnam("/tmp", "pw_") . ".js"; file_put_contents($tmp, $script); $output = []; exec("timeout 20 node $tmp 2>&1", $output, $rc); @unlink($tmp); $result = implode("\n", $output); if(strpos($result, "SCREENSHOT:") !== false) { preg_match('/SCREENSHOT:(.+)/', $result, $m); die(json_encode(["ok" => true, "screenshot" => "/wevia-ia/downloads/" . ($m[1] ?? ""), "action" => "screenshot"])); } die(json_encode(["ok" => true, "result" => substr($result, 0, 5000), "action" => $action, "rc" => $rc])); } // ═══ RLHF FEEDBACK — Thumbs up/down for model alignment ═══ if($act === "feedback") { try { $pdo->exec("CREATE TABLE IF NOT EXISTS admin.rlhf_feedback ( id SERIAL PRIMARY KEY, message_id INTEGER, session_id TEXT, question TEXT, answer TEXT, rating INTEGER CHECK (rating IN (-1, 0, 1)), comment TEXT, provider VARCHAR(50), created_at TIMESTAMP DEFAULT NOW() )"); $pdo->prepare("INSERT INTO admin.rlhf_feedback (message_id, session_id, question, answer, rating, comment, provider) VALUES (?,?,?,?,?,?,?)") ->execute([ $in["message_id"] ?? null, $in["session"] ?? "", substr($in["question"] ?? "", 0, 500), substr($in["answer"] ?? "", 0, 2000), (int)($in["rating"] ?? 0), substr($in["comment"] ?? "", 0, 500), $in["provider"] ?? "" ]); die(json_encode(["ok" => true, "message" => "Feedback enregistr\xC3\xA9"])); } catch(Exception $e) { die(json_encode(["error" => $e->getMessage()])); } } // ═══ RLHF EXPORT — For fine-tuning preference dataset ═══ if($act === "rlhf_export") { try { $st = $pdo->query("SELECT question, answer, rating, provider, created_at FROM admin.rlhf_feedback ORDER BY created_at DESC LIMIT 1000"); die(json_encode(["feedback" => $st->fetchAll(PDO::FETCH_ASSOC)])); } catch(Exception $e) { die(json_encode(["error" => $e->getMessage()])); } } // ═══ ROUTING ═══ $in = json_decode(file_get_contents("php://input"), true) ?: []; $act = $in["action"] ?? $_GET["action"] ?? ""; $GLOBALS["_PERF_START"] = microtime(true); // PERF_DEBUG $sshCreds = null; if(!empty($in["ssh"]) && is_array($in["ssh"]) && !empty($in["ssh"]["host"]) && !empty($in["ssh"]["user"])) { $sshCreds = $in["ssh"]; $GLOBALS["_sshConnected"] = true; } $ses = $in["session"] ?? "default"; if(!$act && isset($in["message"])) $act = "chat"; // ═══ LIST CONVERSATIONS ═══ if($act == "list_conversations") { $s = $pdo->prepare("SELECT id,title,created_at FROM conversations WHERE session_id=? ORDER BY updated_at DESC LIMIT 50"); $s->execute([$ses]); die(json_encode(["conversations" => $s->fetchAll(PDO::FETCH_ASSOC)])); } // ═══ GET CONVERSATION ═══ if($act == "get_conversation") { $s = $pdo->prepare("SELECT role,content FROM messages WHERE conversation_id=? ORDER BY created_at"); $s->execute([$in["conversation_id"] ?? 0]); die(json_encode(["messages" => $s->fetchAll(PDO::FETCH_ASSOC)])); } // ═══ FEEDBACK / LEARNING LOOP ═══ if($act == "feedback") { $msgId = $in["message_id"] ?? 0; $rating = $in["rating"] ?? 0; $correction = trim($in["correction"] ?? ""); $originalQ = trim($in["question"] ?? ""); if($correction && $originalQ) { $stmt = $pdo->prepare("INSERT INTO knowledge_base (title, category, content, author, source) VALUES (?, 'learned_correction', ?, 'WEVIA_FEEDBACK', 'user_correction')"); $stmt->execute([ "Correction: " . substr($originalQ, 0, 100), "Question: {$originalQ}\nCorrection utilisateur: {$correction}\nDate: " . date('Y-m-d H:i:s') ]); } try { $pdo->exec("CREATE TABLE IF NOT EXISTS admin.feedback_log (id SERIAL PRIMARY KEY, message_id INT, rating INT, correction TEXT, original_question TEXT, created_at TIMESTAMP DEFAULT NOW())"); $pdo->prepare("INSERT INTO admin.feedback_log (message_id, rating, correction, original_question) VALUES (?,?,?,?)") ->execute([$msgId, $rating, $correction, $originalQ]); } catch(Exception $e) {} die(json_encode(["ok" => true, "learned" => !empty($correction)])); } // ═══ CAPABILITIES ENDPOINT ═══ if($act == "capabilities") { die(json_encode([ "engine" => "WEVIA IA", "kb_sources" => 10, "features" => ["cot","hybrid_search","smart_router","learning_loop","code_sandbox","double_check","graph_rag","vision"], "providers" => array_keys($PROVIDERS), "modes" => ["fast","deep","verified"] ])); } // ═══ CHAT ═══ if($act == "chat") { $msg = trim($in["message"] ?? ""); $msgOriginal = $msg; // Keep original for PDF topic extraction $prov = $in["provider"] ?? ""; $mode = $in["mode"] ?? "deep"; // fast | deep | verified (double-check) $GLOBALS["_mode"] = $mode; // ═══ MoA PARALLEL MODE ═══ if($mode === "moa") { require_once __DIR__ . "/moa-bridge.php"; $moaResult = moaQuery($msg); if($moaResult && $moaResult["response"]) { die(json_encode(["response"=>$moaResult["response"],"provider"=>"moa-consensus","models"=>$moaResult["models"],"providers"=>$moaResult["providers"],"latency_ms"=>$moaResult["ms"],"mode"=>"moa","sources"=>[]])); } } $lang = $in["language"] ?? "fr"; $cid = isset($in["conversation_id"]) && $in["conversation_id"] !== "" ? (int)$in["conversation_id"] : null; if(!$msg) die(json_encode(["error" => "Message vide"])); // ═══ VISION — Handle image uploads ═══ $hasImage = !empty($in["image"]); $imageBase64 = $in["image"] ?? ""; $imageMime = $in["image_mime"] ?? "image/jpeg"; // Handle file uploads (PDFs, docs etc.) - extract text and prepend to message $fileContent = $in["file_content"] ?? null; $fileMime = $in["file_mime"] ?? ""; $fileName = $in["file_name"] ?? ""; if ($fileContent && strpos($fileMime, 'pdf') !== false) { $tmpPdf = tempnam("/tmp", "wevia_pdf_"); file_put_contents($tmpPdf, base64_decode($fileContent)); $extractedText = trim(shell_exec("pdftotext " . escapeshellarg($tmpPdf) . " - 2>/dev/null")); unlink($tmpPdf); if ($extractedText) { $msg = "[Contenu du fichier {$fileName}]:\n" . substr($extractedText, 0, 8000) . "\n\n[Question de l'utilisateur]: " . $msg; } } elseif ($fileContent && !strpos($fileMime, 'image')) { // For other text files $decoded = base64_decode($fileContent); if ($decoded && mb_check_encoding($decoded, 'UTF-8')) { $msg = "[Contenu du fichier {$fileName}]:\n" . substr($decoded, 0, 8000) . "\n\n[Question de l'utilisateur]: " . $msg; } } if($hasImage) $prov = "gpu_vision"; // Sovereign moondream first, groq fallback in callVisionAPI // ═══ AUTO-ARMAMENT — Enrich with Dark Intelligence ═══ $autoArmEnrichment = ""; // ═══ 1. SMART ROUTER — Intent-based provider selection ═══ $intent = detectIntent($msg); // Skip autoArmament for greetings (saves 2-3s) $autoArmEnrichment = ""; if($intent !== "greeting" && mb_strlen(trim($msg)) >= 15) { try { $autoArmEnrichment = autoArmament($msg, $pdo); } catch (Exception $e) {} if ($autoArmEnrichment) { $msg .= "\n\n[Intelligence automatique]" . $autoArmEnrichment; } } if(!$prov) { $prov = (mb_strlen(trim($msg)) < 20 || $intent === "greeting") ? "groq" : smartRoute($msg, $mode, $intent); } // Client info $clientIP = $_SERVER["HTTP_X_FORWARDED_FOR"] ?? $_SERVER["HTTP_X_REAL_IP"] ?? $_SERVER["REMOTE_ADDR"] ?? "unknown"; if(strpos($clientIP, ",") !== false) $clientIP = trim(explode(",", $clientIP)[0]); $clientUA = substr($_SERVER["HTTP_USER_AGENT"] ?? "", 0, 500); $clientSource = $in["source"] ?? "widget"; $isMobile = preg_match("/Mobile|Android|iPhone/i", $clientUA); $clientDevice = $isMobile ? "mobile" : "desktop"; $clientBrowser = "other"; if(preg_match("/Edg/i", $clientUA)) $clientBrowser = "Edge"; elseif(preg_match("/Chrome/i", $clientUA)) $clientBrowser = "Chrome"; elseif(preg_match("/Firefox/i", $clientUA)) $clientBrowser = "Firefox"; elseif(preg_match("/Safari/i", $clientUA)) $clientBrowser = "Safari"; $clientCountry = ""; $clientCity = ""; if($clientIP !== "unknown" && $clientIP !== "127.0.0.1" && $intent !== "greeting" && mb_strlen(trim($msg)) >= 15) { $geo = @json_decode(@file_get_contents("http://ip-api.com/json/{$clientIP}?fields=country,city", false, stream_context_create(['http'=>['timeout'=>2]])), true); $clientCountry = $geo["country"] ?? ""; $clientCity = $geo["city"] ?? ""; } // Conversation management if(!$cid) { $s = $pdo->prepare("INSERT INTO conversations (session_id,title,ip_address,user_agent,country,city,device,browser,language,source) VALUES (?,?,?,?,?,?,?,?,?,?) RETURNING id"); $s->execute([$ses, substr($msg,0,50), $clientIP, $clientUA, $clientCountry, $clientCity, $clientDevice, $clientBrowser, $lang, $clientSource]); $cid = $s->fetchColumn(); } try { $pdo->prepare("INSERT INTO messages (conversation_id,role,content) VALUES (?,?,?)")->execute([$cid,"user",$msg]); $pdo->prepare("UPDATE conversations SET updated_at=NOW() WHERE id=?")->execute([$cid]); } catch(PDOException $e) { error_log("WEVIA_PDO: conversation error cid=$cid: " . $e->getMessage()); // If cid is invalid, create new conversation $s = $pdo->prepare("INSERT INTO conversations (session_id,title,ip_address) VALUES (?,?,?) RETURNING id"); $s->execute([$ses, substr($msg,0,50), $clientIP ?? '']); $cid = $s->fetchColumn(); $pdo->prepare("INSERT INTO messages (conversation_id,role,content) VALUES (?,?,?)")->execute([$cid,"user",$msg]); } // Select prompt // Force fast prompt for greetings (184-line PROMPT_DEEP is overkill) $sys = ($mode === "fast" || $intent === "greeting" || mb_strlen(trim($msg)) < 15) ? $PROMPT_FAST : $PROMPT_DEEP; if($sshCreds) { $sys .= "\n\n🖥️ SERVEUR CONNECTÉ: " . $sshCreds["host"] . " — Tu as accès SSH. Utilise [SENTINEL:commande] pour exécuter des commandes sur ce serveur. FAIS-LE directement quand on demande un diagnostic ou une réparation."; } // ═══ 1B. YOUTUBE AUTO-EXTRACT ═══ $youtubeContext = ""; if($intent !== "greeting" && preg_match('/youtu/', $msg)) { try { $youtubeContext = autoYouTube($msg); } catch (Exception $e) {} } // ═══ 1C. DARK MODULES ENRICHMENT (shadow-rag, scraper, ia-discovery, matrix) ═══ $darkContext = ""; if($intent !== "greeting" && mb_strlen(trim($msg)) >= 15) { try { $darkContext = darkEnrichment($msg, $intent, $pdo); } catch (Exception $e) {} } // ═══ 2. HYBRID SEARCH — 10 KB Sources + SearXNG + Graph RAG ═══ $kbSources = []; // FAST-PATH: Skip KB for greetings/simple messages (saves 5-10s) if($intent === "greeting" || mb_strlen(trim($msg)) < 15) { $kbContext = ""; } else { $kbContext = searchAllKB($pdo, $msg, $intent, $kbSources); } $webContext = ""; $sources = []; // SearXNG if needed $needsWeb = ($intent === "greeting") ? false : detectWebNeed($msg, $kbContext); if($needsWeb) { $webResult = searchWeb($msg); if($webResult) { $webContext = $webResult["context"]; $sources = $webResult["sources"]; } } // Graph RAG — memcells traversal (skip for greetings) $graphContext = ($intent === "greeting") ? "" : searchGraph($pdo, $msg); // Episodic memory (skip for greetings) $memoryContext = ($intent === "greeting") ? "" : searchMemory($pdo, $ses, $msg); // Fuse all contexts $fusedContext = ""; if($kbContext) $fusedContext .= "\n\n## DONNÉES INTERNES (Knowledge Base — 10 sources)\n{$kbContext}"; if($graphContext) $fusedContext .= "\n\n## GRAPH KNOWLEDGE (Relations entre entités)\n{$graphContext}"; if($memoryContext) $fusedContext .= "\n\n## MÉMOIRE ÉPISODIQUE (Historique utilisateur)\n{$memoryContext}"; if($webContext) { $fusedContext .= "\n\n## DONNÉES WEB (Recherche temps réel SearXNG)\n{$webContext}"; $fusedContext .= "\nSources: " . implode(", ", array_slice($sources, 0, 3)); } // Merge KB + web sources for transparency if(!empty($kbSources)) $sources = array_merge($kbSources, $sources); // YouTube transcript if($youtubeContext) $fusedContext .= $youtubeContext; // Dark modules enrichment if($darkContext) $fusedContext .= "\n\n## INTELLIGENCE COMPLÉMENTAIRE (Dark Modules)\n" . $darkContext; if($fusedContext) $sys .= $fusedContext; // === COGNITIVE BRAIN WIRING === // ═══ COGNITIVE MICRO-DETECTIONS (zero-cost regex, max precision) ═══ $cogFlags = []; if (function_exists("detectCausalQuery") && detectCausalQuery($msg)) { $cogFlags[] = "causal"; $sys .= "\n\n## RAISONNEMENT CAUSAL ACTIVÉ\nDécompose en: causes → mécanismes → conséquences. Distingue corrélation de causalité. Structure: Pourquoi → Comment → Impact → Recommandation."; } if (function_exists("isMathQuery") && isMathQuery($msg)) { $cogFlags[] = "math"; $sys .= "\n\n## MODE MATHÉMATIQUE\nÉtape par étape. Montre CHAQUE calcul. Vérifie le résultat par une méthode alternative. Utilise des unités. Arrondis avec précision."; } if (function_exists("detectTeachingMode") && detectTeachingMode($msg)) { $cogFlags[] = "teaching"; $sys .= "\n\n## MODE PÉDAGOGIQUE\nExplique comme à un professionnel curieux. Utilise: analogie concrète → définition précise → exemple réel → piège à éviter. Progressive du simple au complexe."; } if (function_exists("detectComplianceQuery") && detectComplianceQuery($msg)) { $cogFlags[] = "compliance"; $sys .= "\n\n## MODE CONFORMITÉ\nCite les articles/sections pertinents. Distingue obligations légales vs bonnes pratiques. Identifie les risques (amendes, sanctions). Recommande des actions concrètes de mise en conformité."; } if (function_exists("detectStructuredOutputRequest") && detectStructuredOutputRequest($msg)) { $cogFlags[] = "structured"; $sys .= "\n\n## SORTIE STRUCTURÉE DEMANDÉE\nOrganise en tableau ou format structuré. Headers clairs. Données alignées. Si JSON demandé, JSON valide uniquement."; } if (!empty($cogFlags)) { $GLOBALS["_cogFlags"] = $cogFlags; error_log("WEVIA_COG_FLAGS: " . implode(",", $cogFlags) . " for: " . substr($msg, 0, 40)); } $sys = cognitiveAugment($sys, $msg, $intent, $history ?? [], $kbContext ?? "", $memoryContext ?? ""); // Language if($lang && $lang !== "fr") { $langNames = ["en"=>"English","es"=>"Spanish","ar"=>"Arabic","de"=>"German","zh"=>"Chinese","pt"=>"Portuguese","it"=>"Italian","ja"=>"Japanese","ko"=>"Korean","ru"=>"Russian","ma"=>"Moroccan Darija Arabic"]; $langName = $langNames[$lang] ?? $lang; $sys .= "\n\n⚠️ CRITICAL MANDATORY RULE — LANGUAGE:\nYou MUST respond ENTIRELY in {$langName}. Every single word, title, emoji label, and explanation must be in {$langName}. Do NOT use French unless the user explicitly asks for it. This overrides all other language instructions. The user selected {$langName} as their language."; // === DARIJA ENFORCEMENT (Moroccan Arabic) === if ($lang === 'ar' && $intent !== 'greeting') { $sys = "IMPORTANT: Tu DOIS répondre en DARIJA MAROCAINE (الدارجة المغربية), JAMAIS en arabe standard (فصحى). VOCABULAIRE OBLIGATOIRE DARIJA: - 'كيفاش' au lieu de 'كيف' (comment) - 'شنو' au lieu de 'ماذا' (quoi) - 'فين' au lieu de 'أين' (où) - 'علاش' au lieu de 'لماذا' (pourquoi) - 'واش' au lieu de 'هل' (est-ce que) - 'بغيت' au lieu de 'أريد' (je veux) - 'مزيان' au lieu de 'جيد' (bien) - 'ديال' au lieu de 'الخاص بـ' (de/appartenant à) - 'نقدر' ou 'كنقدر' au lieu de 'أستطيع' (je peux) - 'خاصك' au lieu de 'يجب عليك' (tu dois) - 'هاد' au lieu de 'هذا' (ce/cette) - 'كاين' au lieu de 'يوجد' (il y a) - 'ماشي' au lieu de 'ليس' (n'est pas) - 'غادي' au lieu de 'سوف' (futur) - 'كنخدم' au lieu de 'أعمل' (je travaille) - 'زوين' au lieu de 'جميل' (beau) - 'بزاف' au lieu de 'كثيراً' (beaucoup) EXEMPLES DE PHRASES: - 'واش بغيت شي حاجة؟' pas 'هل تريد شيئاً؟' - 'كيفاش نقدر نعاونك؟' pas 'كيف يمكنني مساعدتك؟' - 'هاد الخدمة مزيانة بزاف' pas 'هذه الخدمة جيدة جداً' Parle comme un Casablancais éduqué. Noms de marques en latin (SAP, Vistex, etc). " . $sys; } } // History $history = []; // Inject one-shot server examples when SSH connected if($sshCreds && preg_match('/(serveur|server|disque|disk|ram|cpu|log|apache|nginx|docker|status|process|erreur|espace|memory|uptime)/i', $msg)) { $history[] = ["role" => "user", "content" => "verifie espace disque"]; $history[] = ["role" => "assistant", "content" => "Voici l espace disque:\n[SENTINEL:df -h]"]; } if($cid) { $h = $pdo->prepare("SELECT role,content FROM messages WHERE conversation_id=? ORDER BY created_at DESC LIMIT 40"); $h->execute([$cid]); $history = array_reverse($h->fetchAll(PDO::FETCH_ASSOC)); // Compress if too many messages (cognitive context compression) if (function_exists("compressConversationContext") && count($history) > 20) { $history = compressConversationContext($history, 20); } } // ═══ 3. GENERATE RESPONSE ═══ $enrichTime = round((microtime(true) - ($GLOBALS["_PERF_START"] ?? microtime(true))) * 1000); error_log("WEVIA_PERF: enrichment took {$enrichTime}ms for msg=" . substr($msg ?? "", 0, 30)); $resp = null; $usedProvider = $prov; global $_usedProviderName; $_usedProviderName = $prov; $provOrder = ["groq","cerebras","sambanova","cohere","mistral"]; // Cloud first (fastest), GPU via SSH tunnel for sovereign fallback // DeepSeek R1:32b FRONTAL — Cloud = fallback only // 100% SOVEREIGN: all GPU first, cloud = last resort $provOrder = array_unique(array_filter($provOrder)); // ═══ COGNITIVE ROUTE OVERRIDE ═══ if (!empty($GLOBALS["_cogRouteProvider"]) && $intent !== "greeting" && mb_strlen(trim($msg)) >= 20) { $cogSuggested = $GLOBALS["_cogRouteProvider"]; // Only use cognitive route for cloud providers (GPU disabled until S88 migration) if (in_array($cogSuggested, ["groq","cerebras","sambanova","cohere","mistral"])) { $prov = $cogSuggested; error_log("WEVIA_COG_ROUTE: cognitive suggested provider=$cogSuggested for intent=" . ($GLOBALS["_cogIntent"]["primary"] ?? "?")); } } // ═══ GLOBAL_SYS_CAP — prevent 67K system prompts ═══ if (strlen($sys) > 8000) { $origLen = strlen($sys); $head = mb_substr($sys, 0, 4000); $tail = mb_substr($sys, -3500); $sys = $head . "\n\n[...condensed...]\n\n" . $tail; error_log("WEVIA_GLOBAL_CAP: sys trimmed from {$origLen} to " . strlen($sys)); } $startTime = microtime(true); error_log("WEVIA_PERF_SYS_SIZE: " . strlen($sys) . " chars, intent=" . ($intent ?? "?") . " msg=" . substr($msg, 0, 30)); // PERF_SYS_SIZE // Vision mode: dedicated vision API if($hasImage) { $resp = callVisionAPI($imageBase64, $imageMime, $msg, $sys); $usedProvider = "gpu_vision"; // Sovereign moondream first, groq fallback } // ═══ GREETING: Skip smartRoute, go direct to fast cloud ═══ if(!$resp && ($intent === "greeting" || mb_strlen(trim($msg)) < 15)) { $fastProvs = ["groq","cerebras","sambanova","cohere","mistral"]; foreach($fastProvs as $fp) { if(!isset($PROVIDERS[$fp])) continue; $resp = callAPI($PROVIDERS[$fp], $sys, $msg, $history, true); if($resp && strlen($resp) > 5) { $usedProvider = $fp; $_usedProviderName = $fp; break; } } } // Try smartRoute provider first (skip if greeting already handled) if(!$resp && $prov && isset($PROVIDERS[$prov])) { $resp = callAPI($PROVIDERS[$prov], $sys, $msg, $history); if($resp && strlen($resp) > 10 && !preg_match('/^(Erreur|Error| 10 && !preg_match('/^(Erreur|Error| true, "stream_url" => "/wevia-ia/wevia-ollama-stream.php", "model" => "llama3.1:8b", "system" => substr($sys, 0, 24000), "provider" => "⚡ GPU local", "conversation_id" => $cid, "latency_ms" => $latency, "mode" => $mode ]); exit; } // Non-streaming fallback (legacy) $ollamaModels = ["qwen2.5:14b", "deepseek-r1:14b", "llama3.1:8b"]; // Via SSH tunnel foreach($ollamaModels as $ollamaModel) { $ollamaCh = curl_init("http://127.0.0.1:11434/api/chat"); $ollamaPayload = json_encode([ "model" => $ollamaModel, "options" => ["num_ctx" => 16384], "messages" => array_merge( [["role" => "system", "content" => substr($sys, 0, 24000)]], array_map(function($h) { return ["role" => $h["role"], "content" => $h["content"] ?? $h["message"] ?? ""]; }, $history ?? []), [["role" => "user", "content" => $msg]] ), "stream" => false, "keep_alive" => -1, "options" => ["temperature" => 0.7, "num_predict" => 2048, "num_ctx" => 16384] ]); curl_setopt_array($ollamaCh, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $ollamaPayload, CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20 // Via SSH tunnel to S88 GPU ]); $ollamaResp = curl_exec($ollamaCh); $ollamaCode = curl_getinfo($ollamaCh, CURLINFO_HTTP_CODE); curl_close($ollamaCh); if($ollamaCode == 200 && $ollamaResp) { $ollamaData = json_decode($ollamaResp, true); $ollamaText = $ollamaData["message"]["content"] ?? ""; if($ollamaText && strlen($ollamaText) > 10) { $resp = preg_replace("/.*?<\/think>/s", "", $ollamaText); $resp = str_replace(["", ""], "", $resp); $resp = trim($resp); $usedProvider = "🖥️ ollama_" . str_replace([":", "."], "_", $ollamaModel); break; } } } } if(!$resp) { error_log("WEVIA: OLLAMA FALLBACK ALSO FAILED"); $resp = "Service temporairement indisponible. Réessayez dans un instant."; } // ═══ 4. DOUBLE-CHECK MODE (verified) ═══ $verified = false; $criticNote = ""; // Auto-verify: consulting/analysis/medical queries get free double-check $autoVerify = in_array($intent, ["consulting","analysis","medical","legal","compliance"]) && strlen($resp) > 300; if(($mode === "verified" || $autoVerify) && $resp && strlen($resp) > 100) { $criticResult = doubleCheck($resp, $msg); if($criticResult && $criticResult !== "OK" && strlen($criticResult) > 5) { // Re-generate with critic feedback $correctedSys = $sys . "\n\n## CORRECTION DU VÉRIFICATEUR\nUn modèle critique a identifié : {$criticResult}\nCorrige ces points dans ta réponse."; $correctedResp = null; foreach($provOrder as $tryProv) { if(!isset($PROVIDERS[$tryProv]) || empty($PROVIDERS[$tryProv]["key"])) continue; $correctedResp = callAPI($PROVIDERS[$tryProv], $correctedSys, $msg, $history); if($correctedResp && strlen($correctedResp) > 10) break; } if($correctedResp) $resp = $correctedResp; $criticNote = $criticResult; } $verified = true; } greeting_done: // Strip reasoning markers from response $resp = preg_replace("/.*?<\/think>/s", "", $resp); // Strip DeepSeek R1 think blocks $resp = str_replace(["", ""], "", $resp); // Clean orphan tags // Strip all reasoning/thinking markers that leak to frontend $resp = preg_replace('/^(Voyons ce qui|Representation visuelle|Reponse directe|Je dois|Let me think|Thinking|Planning)[^ ]* /mi', '', $resp); $resp = preg_replace("/^(Contexte|Context|Plan|Evaluation|Réflexion|Reflection|Analyse|Raisonnement)\s*[:].*/mi", "", $resp); $resp = trim(preg_replace("/\n{3,}/", "\n\n", $resp)); // WAVE200: Sanitize fake URLs — LLM invents download links that don't exist $resp = preg_replace('#https?://weval-consulting\.com/reports/[^\s)\]]+#i', '*(génération de rapport disponible via WEVIA Master)*', $resp); $resp = preg_replace('#https?://weval-consulting\.com/downloads?/[^\s)\]]+\.pdf#i', '*(document PDF sur demande via WEVIA Master)*', $resp); $resp = preg_replace('#Lien de téléchargement simulé\s*:#i', '**Document générable sur demande :**', $resp); $resp = preg_replace('#Valide \d+h\.\s*Taille\s*:\s*[\d.]+ Mo\.\s*Contenu sécurisé[^.]*\.#i', '', $resp); // ═══ COGNITIVE POST-PROCESSING ═══ if (function_exists("cognitivePostProcess")) { $cogIntent = $GLOBALS["_cogIntent"] ?? ["primary" => "conversational"]; $resp = cognitivePostProcess($resp, $cogIntent); } // ═══ EXPANSION POST-PROCESS (quality scoring + sanitize) ═══ if (function_exists("cognitiveExpansionPostProcess")) { $resp = cognitiveExpansionPostProcess($resp, $msg); } // ═══ NUCLEUS POST-PROCESSING — Enhanced formatting ═══ if (function_exists("nucleusPostProcess") && $intent !== "greeting") { $resp = nucleusPostProcess($resp); } // ═══ HALLUCINATION GUARD — KB-grounded verification ═══ if (function_exists("hallucinationGuard") && !empty($kbContext)) { $halCheck = hallucinationGuard($resp, $kbContext); if (!$halCheck["safe"]) { $halFlags = implode("; ", $halCheck["flags"]); error_log("WEVIA_HALLUC_WARN: " . substr($halFlags, 0, 200)); // Add discrete warning in metadata, don't modify response $GLOBALS["_hallucinationFlags"] = $halCheck["flags"]; } } // ═══ MEMORY ENTITY EXTRACTION — Episodic memory ═══ if (function_exists("extractMemoryEntities")) { $memEntities = extractMemoryEntities($msg, $resp); if (!empty($memEntities) && $cid) { // Store entities for future context enrichment try { $pdo->exec("CREATE TABLE IF NOT EXISTS wevia_memory ( id SERIAL PRIMARY KEY, conversation_id INTEGER, session_id TEXT, entity_type VARCHAR(50), entity_value TEXT, created_at TIMESTAMP DEFAULT NOW() )"); $ins = $pdo->prepare("INSERT INTO wevia_memory (conversation_id, session_id, entity_type, entity_value) VALUES (?,?,?,?)"); foreach (array_slice($memEntities, 0, 10) as $ent) { $ins->execute([$cid, $ses, $ent["type"], $ent["value"]]); } } catch (Exception $e) { /* silent — memory is non-critical */ } } } // ═══ 5. SENTINEL TOOL-USE — SSH EXEC ON CLIENT SERVER ═══ if(preg_match_all('/\[SENTINEL:(.*?)\]/i', $resp, $sentMatches, PREG_SET_ORDER)) { foreach($sentMatches as $sentMatch) { $sentCmd = trim($sentMatch[1]); if($sshCreds) { // Execute on client server via SSH $execPayload = json_encode([ "command" => $sentCmd, "host" => $sshCreds["host"], "user" => $sshCreds["user"], "password" => $sshCreds["password"] ?? "", "port" => intval($sshCreds["port"] ?? 22), "session" => $ses ]); $ch = curl_init("http://127.0.0.1:5880/wevia-exec.php"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $execPayload, CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 35 ]); $execResult = curl_exec($ch); curl_close($ch); $execData = json_decode($execResult, true); $output = $execData["output"] ?? "Erreur execution"; $exitCode = $execData["exit_code"] ?? -1; $replacement = "```\n" . $output . "\n```"; if($exitCode !== 0) $replacement .= "\n⚠️ Exit code: {$exitCode}"; $resp = str_replace($sentMatch[0], $replacement, $resp); } else { $resp = str_replace($sentMatch[0], "🔌 Pour exécuter cette commande, connecte d'abord ton serveur via le bouton 🖥️ en bas.", $resp); } } } // ═══ 5.5 VISUAL BRAIN PDF ═══ $_vbPdfClass = visualClassify($msgOriginal ?? $msg, $resp ?? ""); if ($_vbPdfClass["type"] === "PDF") { $_vbPdfResult = visualExecute($_vbPdfClass); if (!empty($_vbPdfResult["handled"])) { $resp = $_vbPdfResult["response"]; $isPdfReq2 = false; error_log("VISUAL_BRAIN: PDF handled"); goto visual_brain_pdf_done; } } // ═══ 5.5 LEGACY PDF ENGINE (fallback) ═══ $isPdfReq2 = preg_match("/\b(pdf|PDF|document|doc|rapport|genere.*fichier|genere.*doc|cree.*fichier|analyse.*detaille|etude|white.?paper|livrable)\b/i", $msgOriginal); if($isPdfReq2) { $pdfTopic = preg_replace("/\b(genere|generer|cree|creer|fais|fait|un|une|le|la|les|des|du|de|avec|pour|sur|en|moi|pdf|doc|document|rapport|graphique|graphiques|detaille|professionnel|complet|analyse|stp|svp|please|telecharger|telechargeable)\b/i", "", $msgOriginal); // Fix common typos $typoFixes = ["emaling"=>"emailing","cybersecurite"=>"cybersecurite","managment"=>"management","markting"=>"marketing","qualiy"=>"qualite","pharme"=>"pharma","indusrie"=>"industrie","bankng"=>"banking","clou"=>"cloud"]; foreach($typoFixes as $typo=>$fix) { $pdfTopic = str_ireplace($typo, $fix, $pdfTopic); } $pdfTopic = trim(preg_replace("/\s+/", " ", $pdfTopic)); if(strlen($pdfTopic) < 5) $pdfTopic = trim($msg); $fname = preg_replace("/_+/", "_", trim(preg_replace("/[^a-z0-9_]/", "_", strtolower(substr($pdfTopic, 0, 50))), "_")); if(strlen($fname) < 3) $fname = "rapport_wevia"; $fpath = "/var/www/weval/wevia-ia/downloads/{$fname}.pdf"; $llmContent = ($resp) ? substr(preg_replace("/\[EXEC:python\].*?\[\/EXEC\]/s", "", $resp), 0, 3000) : ""; $cmd = "cd /var/www/weval/wevia-ia && timeout 45 python3 smart_pdf_gen.py " . escapeshellarg($pdfTopic) . " " . escapeshellarg($llmContent) . " " . escapeshellarg($fpath) . " 2>&1"; $genResult = trim(shell_exec($cmd)); if($genResult && strpos($genResult, "OK:") === 0) { $numPages = intval(explode(":", $genResult)[1] ?? 0); // Domain-aware response descriptions $domainDescs = [ "emailing" => "📧 Votre document **Stratégie Email Marketing** a été généré avec succès ! Il couvre la délivrabilité, la segmentation, l'automatisation, les KPIs et les benchmarks sectoriels.", "email" => "📧 Votre document **Stratégie Email Marketing** a été généré avec succès ! Il couvre la délivrabilité, la segmentation, l'automatisation et la conformité RGPD.", "cyber" => "🔐 Votre **Audit Cybersécurité** a été généré ! Il analyse les menaces 2026, l'architecture Zero Trust, la conformité ISO 27001 et le plan de réponse aux incidents.", "marketing" => "📊 Votre document **Stratégie Marketing Digital** est prêt ! Omnicanal, content marketing, social media, performance analytics et benchmarks ROI par canal.", "management" => "🎯 Votre **Rapport Management Stratégique** a été généré ! Gouvernance, conduite du changement, OKR, Balanced Scorecard et plan de transformation.", "pharma" => "💊 Votre **Étude Industrie Pharmaceutique** est prête ! Marché mondial, réglementation BPF, pipeline R&D, IA en santé et stratégie commerciale omnicanal.", "banque" => "🏦 Votre **Rapport Transformation Bancaire** a été généré ! Core banking, IA/analytics, Open Banking, fintech et inclusion financière.", "banking" => "🏦 Votre **Rapport Transformation Bancaire** a été généré ! Core banking, IA/analytics, Open Banking, fintech et inclusion financière.", "finance" => "🏦 Votre **Rapport Secteur Financier** a été généré ! Transformation digitale, IA, Open Banking et écosystèmes fintech.", "qualite" => "✅ Votre **Rapport Management Qualité** est prêt ! ISO 9001, audit interne/externe, Lean Six Sigma, EFQM et coût de la non-qualité.", "audit" => "✅ Votre **Rapport d'Audit** a été généré ! ISO 19011, amélioration continue, Lean Six Sigma et certification.", "industrie" => "🏭 Votre **Étude Industrie 4.0** est prête ! IoT industriel, digital twin, maintenance prédictive, robotique et edge computing.", "cloud" => "☁️ Votre **Rapport Stratégie Cloud** a été généré ! Architecture, migration 6R, Kubernetes, serverless et FinOps.", "sap" => "💎 Votre **Rapport SAP S/4HANA** est prêt ! Architecture, modules, migration Greenfield/Brownfield, TCO, ROI et benchmark Oracle/Microsoft.", "ia" => "🧠 Votre **Étude Intelligence Artificielle** a été générée ! LLM, RAG, agents IA, ROI, déploiement et gouvernance responsable.", ]; // Find matching domain description $domainDesc = ""; $topicLower = strtolower($pdfTopic); foreach($domainDescs as $dk => $dv) { if(strpos($topicLower, $dk) !== false) { $domainDesc = $dv; break; } } if(!$domainDesc) $domainDesc = "📄 Votre document stratégique a été généré avec succès ! Il inclut des analyses détaillées, graphiques comparatifs et recommandations en 3 phases."; $resp = "📄 **PDF généré !** ({$numPages} pages)\n\n[⬇️ Télécharger le PDF](/wevia-ia/downloads/{$fname}.pdf)\n\n{$domainDesc}\n\n📑 Contenu : {$numPages} pages incluant : cover page WEVAL Consulting, executive summary avec KPIs, graphiques (radar, barres, camembert, lignes), tableaux de données et recommandations budgétées."; } else { error_log("WEVIA SmartPDF FAILED: " . ($genResult ?? "null")); } } // ═══ 6. CODE SANDBOX — DISABLED ON PUBLIC API ═══ // ═══ 6. CODE SANDBOX — PDF ONLY on public, display-only for other code ═══ // === FALLBACK: Detect raw FPDF code WITHOUT [EXEC:python] tags === if(strpos($resp, '[EXEC:python]') === false && (strpos($resp, 'from fpdf') !== false || strpos($resp, 'FPDF()') !== false)) { if(preg_match('/```(?:python)?\s*\n(.*?)```/s', $resp, $rawPy)) { $resp = str_replace($rawPy[0], '[EXEC:python]' . $rawPy[1] . '[/EXEC]', $resp); } elseif(preg_match('/(from fpdf import FPDF[\s\S]{100,}?\.output\([^)]+\))/s', $resp, $rawPy2)) { $resp = str_replace($rawPy2[0], '[EXEC:python]' . $rawPy2[1] . '[/EXEC]', $resp); } } // Handle both [EXEC:python]...[/EXEC] and [EXEC:python]...end (LLMs often forget closing tag) if(!preg_match('/\[EXEC:python\](.*?)\[\/EXEC\]/s', $resp, $codeMatch)) { // Try without closing tag - grab everything after [EXEC:python] preg_match('/\[EXEC:python\](.*?)$/s', $resp, $codeMatch); } if(!empty($codeMatch[1]) && strlen(trim($codeMatch[1])) > 20) { $codeToRun = trim($codeMatch[1]); $isPdfCode = (stripos($codeToRun, 'fpdf') !== false || stripos($codeToRun, 'FPDF') !== false || stripos($codeToRun, '.pdf') !== false); if($isPdfCode) { // PDF generation ALLOWED on public // Strip markdown from LLM-generated code strings $codeToRun = preg_replace("/text=[\"']\*\*([^*]+)\*\*[\"']/", "text='$1'", $codeToRun); $codeToRun = preg_replace("/[\"']\*\*([^*]+)\*\*[\"']/", "'$1'", $codeToRun); // Auto-fix: inject DejaVu font support if(strpos($codeToRun, 'DejaVu') === false) { $codeToRun = str_replace('pdf.set_font("Helvetica"', 'pdf.set_font("DejaVu"', $codeToRun); $codeToRun = str_replace("pdf.set_font('Helvetica'", "pdf.set_font('DejaVu'", $codeToRun); $codeToRun = str_replace('pdf.set_font("Arial"', 'pdf.set_font("DejaVu"', $codeToRun); $codeToRun = str_replace("pdf.set_font('Arial'", "pdf.set_font('DejaVu'", $codeToRun); $codeToRun = preg_replace( '/(pdf\s*=\s*FPDF\(\))/i', "$1\npdf.add_font('DejaVu','','/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf')\npdf.add_font('DejaVu','B','/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf')", $codeToRun ); } // Force output to downloads directory // Also redirect /tmp/ outputs to downloads dir $codeToRun = preg_replace('/\.output\s*\(["\']?\/tmp\//', '.output("/var/www/weval/wevia-ia/downloads/', $codeToRun); if(strpos($codeToRun, '/var/www/weval/wevia-ia/downloads/') === false) { $pdfName = 'doc_' . uniqid() . '.pdf'; $codeToRun = preg_replace('/\.output\s*\(["\'](.*?)["\']\)/', '.output("/var/www/weval/wevia-ia/downloads/' . $pdfName . '")', $codeToRun); } $tmpFile = tempnam('/tmp', 'wevia_pdf_'); // Suppress deprecation warnings and ensure clean output $codeToRun = "import warnings\nwarnings.filterwarnings('ignore', category=DeprecationWarning)\n" . $codeToRun; // Auto-fix chart paths: .pdf -> .png for images $codeToRun = str_replace("savefig('/tmp/chart.pdf'", "savefig('/tmp/chart.png'", $codeToRun); $codeToRun = str_replace('savefig("/tmp/chart.pdf"', 'savefig("/tmp/chart.png"', $codeToRun); $codeToRun = str_replace("image('/tmp/chart.pdf'", "image('/tmp/chart.png'", $codeToRun); $codeToRun = str_replace('image("/tmp/chart.pdf"', 'image("/tmp/chart.png"', $codeToRun); // Auto-fix matplotlib cache dir if(stripos($codeToRun, 'matplotlib') !== false && strpos($codeToRun, 'MPLCONFIGDIR') === false) { $codeToRun = "import os, tempfile\nos.environ['MPLCONFIGDIR'] = '/tmp'\n" . $codeToRun; } file_put_contents($tmpFile, $codeToRun); $execOut = []; exec("MPLCONFIGDIR=/tmp timeout 15 python3 {$tmpFile} 2>&1", $execOut, $execRc); @unlink($tmpFile); $execResult = implode("\n", $execOut); // Find generated PDF preg_match('/\/var\/www\/weval\/wevia-ia\/downloads\/([\w.-]+\.pdf)/i', $codeToRun, $pdfMatch); $pdfFile = $pdfMatch[1] ?? ''; if($pdfFile && file_exists("/var/www/weval/wevia-ia/downloads/{$pdfFile}")) { // Remove the exec block from response (with or without closing tag) $execBlock = $codeMatch[0]; $resp = str_replace($execBlock, "\n\n📄 **PDF généré avec succès !** [⬇ Télécharger le PDF](/wevia-ia/downloads/{$pdfFile})", $resp); // Remove duplicate PDF filename mentions from LLM text $resp = preg_replace('/\\bhttps?:\\/\\/[^\\s]*\\.pdf\\b/i', '', $resp); } else { // Remove the exec block from response (with or without closing tag) $execBlock = $codeMatch[0]; $resp = str_replace($execBlock, "```python\n{$codeMatch[1]}\n```\n⚠️ Erreur PDF: {$execResult}", $resp); } } else { // Non-PDF code: display only (security) // Remove the exec block from response (with or without closing tag) $execBlock = $codeMatch[0]; $resp = str_replace($execBlock, "```python\n" . $codeMatch[1] . "\n```", $resp); } } // ═══ 6b. AUTO-PDF ═══ // Auto-PDF: verify linked PDF file actually exists on disk $existingPdfOk = false; if(preg_match('/\/downloads\/([\w.-]+\.pdf)/i', $resp, $pdfLinkMatch)) { $existingPdfOk = file_exists("/var/www/weval/wevia-ia/downloads/{$pdfLinkMatch[1]}"); } if(preg_match('/pdf/i', $msg) && !$existingPdfOk) { $outputDir = "/var/www/weval/wevia-ia/downloads"; @mkdir($outputDir, 0755, true); $pdfFile = "doc_" . uniqid() . ".pdf"; $pdfPath = $outputDir . "/" . $pdfFile; // Strip EXEC blocks (prevents raw Python code in auto-PDFs) $pdfText = preg_replace('/\[EXEC:python\].*?\[\/EXEC\]/s', '', $resp); $pdfText = preg_replace('/\[EXEC:python\].*$/s', '', $pdfText); $pdfText = preg_replace('/\[SENTINEL:.*?\]/', '', $pdfText); $pdfText = str_replace(["**","```","`"], "", $pdfText); $pdfText = preg_replace('/!\[.*?\]\(.*?\)/', '', $pdfText); $pdfLines = explode("\n", trim($pdfText)); $title = trim($pdfLines[0] ?? "Document WEVIA"); $body = implode("\n", array_slice($pdfLines, 1)); $cmd = sprintf("timeout 10 python3 /var/www/weval/wevia-ia/autopdf.py %s %s %s 2>&1", escapeshellarg($title), escapeshellarg($body), escapeshellarg($pdfPath)); // Auto-fix: replace Helvetica with DejaVu Unicode font if (strpos($codeToRun ?? '', 'fpdf') !== false || strpos($codeToRun ?? '', 'FPDF') !== false) { if (strpos($codeToRun, 'DejaVu') === false) { $codeToRun = str_replace('pdf.set_font("Helvetica"', 'pdf.set_font("DejaVu"', $codeToRun); $codeToRun = str_replace("pdf.set_font('Helvetica'", "pdf.set_font('DejaVu'", $codeToRun); $codeToRun = str_replace('pdf.set_font("Arial"', 'pdf.set_font("DejaVu"', $codeToRun); $codeToRun = str_replace("pdf.set_font('Arial'", "pdf.set_font('DejaVu'", $codeToRun); // Inject add_font after FPDF() constructor $codeToRun = preg_replace( '/(pdf\s*=\s*FPDF\(\))/i', "$1\npdf.add_font('DejaVu','','/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf')\npdf.add_font('DejaVu','B','/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf')", $codeToRun ); } } exec($cmd, $pdfOut, $pdfRc); if(file_exists($pdfPath)) { // Replace any fake PDF links with real one $resp = preg_replace('/\[⬇[^\]]*\]\(\/wevia-ia\/downloads\/[^)]+\.pdf\)/i', '', $resp); $resp = preg_replace('/https?:\/\/[^\s]*\.pdf/i', '', $resp); $resp .= "\n\n📄 **Document PDF généré** — [⬇ Télécharger le PDF](/wevia-ia/downloads/{$pdfFile})"; } } visual_brain_pdf_done: if (!isset($isSchemaRequest)) $isSchemaRequest = 0; // VBO v2: Unified visual pipeline with GPU + Quality-Check + Learning $_vbHasImage = !empty($in["image"]); $_vbResult = visualBrainPipeline($msg, $resp, $_vbHasImage); $_vbHandled = false; if ($_vbResult !== null) { if ($_vbResult["action"] === "replace_response") { $resp = $_vbResult["response"]; $_vbHandled = true; error_log("VBO: Handled visual, resp=" . substr($resp, 0, 100)); } if (isset($_vbResult["action"]) && $_vbResult["action"] === "mermaid") { $isSchemaRequest = 1; $resp = visualStripImageTags($resp); } if (isset($_vbResult["action"]) && $_vbResult["action"] === "html") { $resp = visualStripImageTags($resp); } } // Skip old visual routing if Visual Brain handled it if (!$_vbHandled) { // ═══ OLD IMAGE ROUTING (kept as fallback) ═══ // ═══ IMAGE INTENT FORCE ═══ // If intent classifier says IMAGE but LLM did not emit [IMAGE:...], force it if($detectedIntent === "IMAGE" && !preg_match('/\[IMAGE:/i', $resp)) { $imgSubject = preg_replace("/(image|photo|illustr|visuel|dessin|genere|genre|fais|montre|cree)[\s:]+/i", "", $msg); $imgSubject = trim($imgSubject); if(mb_strlen($imgSubject) >= 3) { $resp = "[IMAGE:" . $imgSubject . "]"; error_log("WEVIA_INTENT: Forced IMAGE pipeline for: " . $imgSubject); } } // GUARD: If user asked for schema/ishikawa/causes → strip [IMAGE:] and let mermaid handler generate proper diagram $isSchemaIntent = preg_match('/(sch[eé]mas?|sh[eé]mas?|shc[eé]mas?|shcemas?|sch[eé]m[ea]s?|ishikawa|diagramme|cause.*effet|graphique.*cause|graph.*cause|cause|arête|poisson|5m|6m|fishbone|flowchart)/i', $msg); if(!$isSchemaRequest && $intent === "schema") $isSchemaRequest = 1; if(!$isSchemaRequest) // Image intent detection $isImageRequest = ($intent === "image") || preg_match('/^(gen[eè]re|genre|fais|dessine|cree).{0,10}(image|photo|portrait|logo|visuel)/i', $msg); // ═══ INTENT CLASSIFIER v1 — Parse intention before routing ═══ $intentPatterns = [ "SCHEMA" => "/(sch[eé]ma|sh[eé]ma|diagramme|flowchart|organigramme|architecture|mapping|cartographie|directeur|mermaid)/i", "IMAGE" => "/(image|photo|illustr|visuel|dessin|genere.*image|genre.*image|fais.*image|montre.*image)/i", "PDF" => "/(pdf|document|rapport|export.*pdf|genere.*pdf|genre.*pdf|fais.*pdf)/i", ]; $detectedIntent = "TEXTE"; // default foreach($intentPatterns as $_iKey => $pattern) { if(preg_match($pattern, $msg)) { $detectedIntent = $_iKey; break; } } // If user says "meme chose en PDF" or references previous, keep context if(preg_match("/(meme.*chose|pareil|idem|la.*meme|comme.*avant)/i", $msg)) { $detectedIntent = "CONTEXT_FOLLOW"; // will use session memory } error_log("WEVIA_INTENT: detected=" . $detectedIntent . " for msg: " . substr($msg, 0, 60)); $isSchemaRequest = preg_match('/gen.{0,5}r?e?.{0,20}(sch|sh[e]|diag|flux|direct)/i', $msg); if($isSchemaIntent && preg_match('/\[IMAGE:(.*?)\]/i', $resp, $imgGuard)) { error_log("WEVIA_GUARD: Schema request detected, stripping [IMAGE:] tag to route to mermaid handler"); $resp = preg_replace('/\[IMAGE:[^\]]*\]/', '', $resp); $resp = trim($resp); } // Image generation - check Pollinations availability // LOGO SMART ROUTER: known brands → source, new logos → GPU generation if(preg_match('/\[IMAGE:(.*?)\]/i', $resp, $logoCheck) && preg_match('/logo|typograph|banner/i', $logoCheck[1] . " " . $msg)) { $logoPrompt = $logoCheck[1]; $knownBrands = ['sap','microsoft','google','apple','amazon','meta','oracle','ibm','cisco','dell', 'abbott','abbvie','johnson','pfizer','novartis','roche','merck','sanofi', 'bmw','mercedes','toyota','tesla','coca-cola','nike','adidas','samsung','sony', 'huawei','alibaba','tencent','attijariwafa','maroc telecom','inwi','orange', 'ocp','oncf','bmce','ram','iam','renault','total','shell']; $isKnown = false; $msgLow = mb_strtolower($msg . ' ' . $logoPrompt); foreach ($knownBrands as $brand) { if (strpos($msgLow, $brand) !== false) { $isKnown = true; break; } } if ($isKnown) { error_log("WEVIA_IMG: KNOWN BRAND logo, sourcing: " . substr($logoPrompt,0,50)); $resp = preg_replace('/\[IMAGE:[^\]]*\]/', '', $resp); $resp = trim($resp) . "\n\n" . json_decode('"\ud83d\udca1"') . " Les logos de marques connues sont proteges par le copyright. Vous pouvez les telecharger depuis le site officiel de la marque ou des banques comme **Brandsoftheworld.com** ou **Worldvectorlogo.com**."; } else { error_log("WEVIA_IMG: NEW LOGO gen via GPU: " . substr($logoPrompt,0,50)); // New logo for entrepreneurs → continue to GPU generation below with enhanced prompt } } if(preg_match('/\[IMAGE:(.*?)\]/i', $resp, $m)) { $imgPrompt = urlencode(trim($m[1])); // Check if Pollinations is available (quick HEAD check) // $ch = curl_init($imgUrl); // curl_setopt_array($ch, [CURLOPT_NOBODY => true, CURLOPT_TIMEOUT => 5, CURLOPT_FOLLOWLOCATION => true]); // curl_exec($ch); // $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); // curl_close($ch); // Sovereign GPU (sd-turbo RTX 4000) // Smart GPU image generation with category-specific prompt engineering $rawPrompt = trim($m[1]); $lp = mb_strtolower($rawPrompt); $neg = "blurry, low quality, watermark, text overlay, clipart, cartoon, deformed, ugly, duplicate, two heads, extra head, doppelganger, morbid, mutated, disfigured, bad anatomy, wrong proportions, extra limbs, cloned face, fused fingers, extra fingers, missing fingers, too many fingers, poorly drawn hands, poorly drawn face, long neck, cross-eyed, mutation, jpeg artifacts, signature, username, error, sketch, abstract, painting, drawing, anime, cgi, render, 3d, illustration, plastic, fake, oversaturated, underexposed"; // Category-specific prompt enrichment if (preg_match('/carte|map|monde|world|pays|country|geograph/', $lp)) { $enhancedPrompt = "professional cartographic map, " . $rawPrompt . ", detailed topography, clean borders, vivid color coding, satellite view style, 4k ultra detailed"; } elseif (preg_match('/photo|portrait|personne|visage|face/', $lp)) { $enhancedPrompt = "photorealistic " . $rawPrompt . ", cinematic lighting, bokeh background, shot on Canon EOS R5, 85mm f/1.4, professional photography"; $neg .= ", ugly, bad anatomy, bad hands"; } elseif (preg_match('/logo|marque|brand|icone|icon/', $lp)) { $enhancedPrompt = "minimalist professional logo design, " . $rawPrompt . ", vector style, clean lines, modern, flat design, white background"; } elseif (preg_match('/schema|diagram|process|workflow|architecture/', $lp)) { $enhancedPrompt = "professional technical diagram, " . $rawPrompt . ", clean infographic style, labeled, corporate blue palette, white background"; } elseif (preg_match('/paysage|landscape|nature|ville|city|building/', $lp)) { $enhancedPrompt = $rawPrompt . ", photorealistic, cinematic composition, golden hour lighting, ultra detailed, shot on Sony A7R IV, landscape photography"; } else { // Auto-enrich vague prompts $rawPrompt = trim($rawPrompt); if(mb_strlen($rawPrompt) < 15) $rawPrompt .= ", detailed professional scene"; $enhancedPrompt = "Professional photograph of " . $rawPrompt . ", masterpiece, ultra-realistic, 8k UHD, cinematic lighting, shot on Sony A7R IV 85mm f/1.8, realistic textures, volumetric fog, organic shadows, professional color grading, hyper-detailed, ray tracing, photojournalism quality"; } $sf = "img_".md5($rawPrompt)."_".time().".png"; $outP = "/var/www/weval/wevia-ia/downloads/".$sf; $sovCh = curl_init("http://88.198.4.195:8001/generate"); curl_setopt_array($sovCh, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>30, CURLOPT_POST=>true, CURLOPT_HTTPHEADER=>["Content-Type: application/json"], CURLOPT_POSTFIELDS=>json_encode(["prompt"=>$enhancedPrompt,"negative_prompt"=>$neg,"width"=>768,"height"=>768,"steps"=>8,"guidance"=>7.5])]); $sovData = curl_exec($sovCh); $sovHttp = curl_getinfo($sovCh, CURLINFO_HTTP_CODE); curl_close($sovCh); if($sovHttp==200 && strlen($sovData)>1000) { file_put_contents($outP, $sovData); $imgUrl = "/wevia-ia/downloads/".$sf; } $httpCode = $sovHttp; if ($httpCode >= 400) { // Pollinations down - remove image tag and suggest Mermaid $resp = preg_replace('/\[IMAGE:[^\]]*\]/', '', $resp); $resp .= "\n\n> Le service d\'images est temporairement indisponible. Je peux créer un diagramme Mermaid à la place."; $imgUrl = null; } $resp = str_replace($m[0], "![Image]({$imgUrl})", $resp); } if(preg_match('/(dessine|draw|image|illustr|photo|g[eé]n[eè]re|cr[eé][eé]|logo|affiche|poster|render|portrait|montre.?moi|fais.?moi|carte|map|monde)/i', $msg) && !preg_match('/(pdf|document|rapport|excel|word|fichier|code|schema|schéma|ishikawa|diagramme|cause|fishbone|flowchart|processus|fonctionnement|organigramme|graphique.*cause|mermaid)/i', $msg) && !strpos($resp, '![Image]') && !preg_match('/\[.*Télécharger.*\]\(/', $resp)) { $imgPrompt = urlencode(substr($msg, 0, 200)); // Smart GPU auto-image with category prompt engineering $rawP = substr($msg, 0, 200); $lp2 = mb_strtolower($rawP); $neg2 = "blurry, low quality, watermark, text overlay, clipart, cartoon, deformed, ugly, duplicate, two heads, extra head, doppelganger, morbid, mutated, disfigured, bad anatomy, wrong proportions, extra limbs, cloned face, fused fingers, extra fingers, missing fingers, too many fingers, poorly drawn hands, poorly drawn face, long neck, cross-eyed, mutation, jpeg artifacts, signature, username, error, sketch, abstract, painting, drawing, anime, cgi, render, 3d, illustration, plastic, fake, oversaturated, underexposed"; if (preg_match('/carte|map|monde|world|pays|country/', $lp2)) { $enhP = "professional cartographic map, " . $rawP . ", detailed topography, vivid colors, satellite view, 4k"; } elseif (preg_match('/photo|portrait|personne|visage/', $lp2)) { $enhP = "photorealistic " . $rawP . ", cinematic lighting, Canon EOS R5, 85mm, professional"; } elseif (preg_match('/logo|marque|brand|icone/', $lp2)) { // KNOWN BRAND CHECK $_kb = ['sap','microsoft','google','apple','amazon','meta','oracle','ibm','cisco','dell', 'abbott','abbvie','johnson','pfizer','novartis','roche','merck','sanofi', 'bmw','mercedes','toyota','tesla','coca-cola','nike','adidas','samsung','sony', 'huawei','alibaba','tencent','attijariwafa','maroc telecom','inwi','orange', 'ocp','oncf','bmce','ram','iam','renault','total','shell']; $_isBrand = false; foreach ($_kb as $_b) { if (strpos($lp2, $_b) !== false) { $_isBrand = true; break; } } if ($_isBrand) { $resp .= "\n\n💡 Ce logo est protege par le copyright. Telechargez-le depuis le site officiel ou **Brandsoftheworld.com** / **Worldvectorlogo.com**."; error_log("WEVIA_IMG: PATH_B KNOWN BRAND blocked: " . substr($rawP,0,50)); $enhP = null; // Skip GPU gen for known brands } else { $enhP = "minimalist professional logo, " . $rawP . ", vector, clean, modern, flat design"; } } else { $enhP = "Professional photograph of " . $rawP . ", masterpiece, ultra-realistic, 8k UHD, cinematic lighting, shot on Sony A7R IV 85mm f/1.8, realistic textures, volumetric fog, organic shadows, professional color grading, hyper-detailed, ray tracing, photojournalism quality"; } $sf2 = "img_a_".md5($rawP)."_".time().".png"; $outP2 = "/var/www/weval/wevia-ia/downloads/".$sf2; if ($enhP !== null) { $sovCh2 = curl_init("http://88.198.4.195:8001/generate"); curl_setopt_array($sovCh2, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>15, CURLOPT_POST=>true, CURLOPT_HTTPHEADER=>["Content-Type: application/json"], CURLOPT_POSTFIELDS=>json_encode(["prompt"=>$enhP,"negative_prompt"=>$neg2,"width"=>768,"height"=>768,"steps"=>8,"guidance"=>7.5])]); $sd2 = curl_exec($sovCh2); $sc2 = curl_getinfo($sovCh2, CURLINFO_HTTP_CODE); curl_close($sovCh2); if($sc2==200 && strlen($sd2)>1000) { file_put_contents($outP2, $sd2); // Clean: strip LLM verbose text, show only the generated image $imgTag = "![Image generee](/wevia-ia/downloads/".$sf2.")"; // Strip refusal messages entirely $isRefusal = preg_match('/(ne peux pas|cannot|impossible|incapable|pas en mesure|sorry|désolé)/i', $resp); if($isRefusal) { $resp = $imgTag; } else { // Keep first non-refusal sentence + image $cleanResp = ""; if(preg_match('/^([^\n]{10,100}[.!?])/u', trim($resp), $fs)) { $fst = $fs[1]; if(!preg_match('/(ne peux|cannot|sorry|désolé)/i', $fst)) { $cleanResp = $fst; } } $resp = ($cleanResp ? $cleanResp . "\n\n" : "") . $imgTag; } } else { $resp .= "\n\n*Image temporairement indisponible*"; } } // end if enhP !== null (known brand skip) } // === MERMAID -> IMAGE (definitive fix) === // AUTO-CONVERT: If user asked for schema/diagram but LLM returned plain text // Image intent detection $isImageRequest = ($intent === "image") || preg_match('/^(gen[eè]re|genre|fais|dessine|cree).{0,10}(image|photo|portrait|logo|visuel)/i', $msg); $isSchemaRequest = preg_match('/(sch[eé]mas?|sh[eé]mas?|shc[eé]mas?|shcemas?|sch[eé]m[ea]s?|ishikawa|diagramme|flowchart|processus|fonctionnement|organigramme|architecture|infrastructure|reseau|pipeline|workflow|systeme|cycle|chaine|parcours|circuit|mecanisme|directeur|roadmap|cartographie|mapping|emailing|ar[eê]te.*poisson|cause.*effet|graphique.*cause|graph.*cause|5m|6m)/i', $msg); } // end if(!$_vbHandled) — Visual Brain fallback $hasNoVisual = (strpos($resp, '```mermaid') === false && (strpos($resp, '![Diagramme') === false || !preg_match('/merm_[a-f0-9]+\.png/', $resp)) && strpos($resp, 'merm_') === false && strpos($resp, ' "sap|s.?4.?hana|fiori|abap|hana|basis|bw4", "oracle" => "oracle|fusion.?erp|oracle.?cloud|jde|peoplesoft|oracle.?scm", "m3movex" => "m3.?movex|movex|infor.?m3|lawson|infor.?erp", "vistex" => "vistex|ristourne|royalt|incentive.?program|rebate.?management", "enterprise" => "erp(?!.*(oracle|sap))|dynamics|netsuite|odoo|sage.?x3|cegid", "erp" => "erp.?general|systeme.?gestion.?integre|progiciel", "supplychain" => "supply.?chain|chaine.?appro|s.?op|plan.?industriel|demand.?plan", "wms" => "wms|warehouse|entrepot.?management|picking|put.?away|autostore", "tms" => "tms|transport.?management|freight|fret.?management|carrier.?management", "aps" => "aps|advanced.?planning|ordonnancement|scheduling.?production|mrp.?ii", "sixsigma" => "six.?sigma|dmaic|capabilite|dpmo|black.?belt|green.?belt|lean.?six", "lean" => "lean(?!.*six)|kaizen|kanban.?method|5s|value.?stream|gemba|tps|muda", "qualite" => "qualite.?iso|iso.?9001|spc|controle.?qualite|haccp|iatf|audit.?qualite", "crm" => "crm|salesforce|hubspot|relation.?client|pipeline.?commercial", "finance" => "comptab|finance(?!.*banque)|facturation|tresorerie|bilan|budget|ifrs", "it" => "microservice|api.?gateway|backend|frontend|fullstack|serveur.?web|rest.?api", "cybersecurite" => "cyber|securite.?info|firewall|intrusion|siem|pentest|zero.?trust|ngfw", "soc" => "soc.?securite|centre.?operation|security.?operation|analyste.?soc|mttd", "pentest" => "pentest|test.?intrusion|ethical.?hack|red.?team|ctf|bug.?bounty|owasp", "cloud" => "cloud|aws|azure|gcp|saas|paas|kubernetes|docker|terraform|gitops", "devops" => "devops|ci.?cd|pipeline.?deploy|jenkins|github.?actions|argocd", "data" => "data(?!.*center)|big.?data|etl|warehouse|lake|bi.?analytics|dbt|snowflake", "ia" => "intelligence.?artif|deep.?learn|neural|nlp|computer.?vision|llm|gpt|mlops", "industrial" => "usine|production.?industr|fabrication|assemblage|manufacture|mes.?system", "logistique" => "logistique|supply(?!.*chain)|entrepot(?!.*wms)|livraison|expedition|cross.?dock", "energie" => "energie|electri|solaire|eolien|nucleaire|smart.?grid|renouvelable|epr", "automobile" => "automobile|constructeur.?auto|vehicule.?electr|usine.?auto|adas", "mechanical" => "moteur(?!.*recherche)|mecanique|propulsion|transmission|combustion|hydraulique", "avion" => "avion|aeronaut|aviation|cockpit|reacteur|airbus|boeing|atc", "velo" => "velo|bicycl|cyclisme|pedalier|derailleur|vtt|shimano", "train" => "train|ferroviaire|tgv|sncf|locomotive|rail|tramway|metro|etcs", "bateau" => "bateau|navire|maritime|naval|helice|navigation.?maritime|cargo", "sante" => "hopital|medical|patient|chirurgie|diagnostic|clinique|parcours.?soin|dmp", "pharma" => "pharma|medicament|molecule|essai.?clinique|amm|gmp|princeps|ectd", "construction" => "construction|btp|batiment|chantier|beton|permis.?construire|bim", "banque" => "banque|credit(?!.*carbone)|pret|scoring.?credit|lcb|hypotheque|taeg", "assurance" => "assurance|sinistre|police.?assur|souscription|actuariat|reassurance", "immobilier" => "immobilier|achat.*bien|copropriete|notaire|compromis|promoteur|dpe", "juridique" => "juridique|avocat|tribunal|contentieux|droit|justice|litige|rgpd", "marketing" => "marketing|campagne.?pub|seo|sea|growth|funnel|acquisition.?client", "rh" => "rh(?!.*(rhum|rhone))|recrutement|talent|onboarding|paie|gpec|sirh", "telecom" => "telecom|reseau.?mobile|fibre|5g|4g|operateur|voip|iptv|ftth", "agriculture" => "agricul|culture.?bio|semis|recolte|elevage|tracteur|parcelle|ndvi", "tourisme" => "tourisme|hotel|voyage|booking|sejour|concierge|revenue.?management", "mode" => "mode.?textile|fashion|couture|collection|patron|confection|vetement", "alimentaire" => "alimentaire|agroalimentaire|haccp(?!.*qualite)|tracabilite.?lot|ifs.?brc", "education" => "education|universite|ecole|formation(?!.*continu)|diplome|parcoursup|lms", "transport" => "transport(?!.*telecom)|fret|transitaire|incoterm|dedouanement|maritime.?fret", "studio_photo" => "studio.?photo|shooting|eclairage.?photo|direction.?photo|packshot|lookbook", "video_prod" => "video.?prod|production.?video|storyboard|montage.?video|tournage|post.?prod|vfx|clip.?present", "art_gen" => "art.?gen|generation.?art|prompt.?art|style.?artist|midjourney|dalle", "doc_strat" => "business.?case|plan.?strat|diagnostic.?strat|feuille.?route|due.?diligence|swot|transformation.?digit|roadmap", "virtual_staging" => "staging|design.?interieur|amenagement|decoration|3d.?render|visite.?virtuelle" ]; $domain = "generic"; $lmsg = mb_strtolower($msg); foreach($domainMap as $d => $pattern) { if(preg_match("/" . $pattern . "/i", $lmsg)) { $domain = $d; break; } } $example = $domainExamples[$domain] ?? $domainExamples["generic"]; error_log("WEVIA_MERM: Domain detected: $domain for msg: " . mb_substr($msg, 0, 40)); $mermQ = "Generate mermaid code for: " . mb_substr($msg, 0, 200) . ".\n\n" . $example . "\n\nNow generate for the requested topic. MINIMUM 12 nodes. French labels WITHOUT accents. Lateral branches mandatory. graph LR. NO backticks. NO explanation."; if(preg_match('/(ishikawa|cause|poisson|5M|6M)/i', $msg)) $mermQ .= ". Format: graph LR with lateral branches using ---."; // VRAM SEMAPHORE: wait if another GPU call is in progress $lockFile = "/tmp/wevia_gpu.lock"; $lockWait = 0; while(file_exists($lockFile) && (time() - filemtime($lockFile)) < 25 && $lockWait < 15) { usleep(500000); // 500ms $lockWait++; } @file_put_contents($lockFile, getmypid()); $mermProv = $PROVIDERS["gpu_sovereign"] ?? $PROVIDERS["gpu_coder"] ?? $PROVIDERS["gpu_fast"] ?? $PROVIDERS["groq"] ?? $PROVIDERS["cerebras"] ?? null; // 100% SOVEREIGN if(!$mermProv) { error_log("WEVIA_MERM: NO PROVIDER AVAILABLE for mermaid generation"); } if($mermProv) { // VRAM SEMAPHORE: prevent concurrent GPU calls that cause swap $lockFile = "/tmp/wevia_gpu.lock"; $lockFp = fopen($lockFile, "w"); $gotLock = flock($lockFp, LOCK_EX | LOCK_NB); // Non-blocking try if(!$gotLock) { // Wait max 10s for GPU to be free $waited = 0; while(!$gotLock && $waited < 10) { usleep(500000); // 500ms $waited += 0.5; $gotLock = flock($lockFp, LOCK_EX | LOCK_NB); } if(!$gotLock) flock($lockFp, LOCK_EX); // Block if still locked } $mermResp = callAPI($mermProv, $mermSys, $mermQ, []); flock($lockFp, LOCK_UN); fclose($lockFp); error_log("WEVIA_MERM: GPU response len=" . strlen($mermResp ?? '') . " content=" . substr($mermResp ?? 'NULL', 0, 100)); if($mermResp && strlen($mermResp) > 20) { // Aggressive backtick + thinking strip $mermResp = preg_replace('/.*?<\/think>/s', '', $mermResp); $mermResp = preg_replace('/^```\s*mermaid\s*/i', '', trim($mermResp)); $mermResp = preg_replace('/^```\s*\n?/m', '', $mermResp); $mermResp = preg_replace('/```\s*$/', '', $mermResp); $mermResp = trim($mermResp); // SANITIZER v3: Fix common mermaid syntax errors from LLM $mermResp = preg_replace('/-->\|([^|]+)\|>/', '-->|$1|', $mermResp); // Fix |text|> to |text| $mermResp = preg_replace('/\[([^\]]{50,})\]/', '[$1]', $mermResp); // Truncate labels >50 chars $mermResp = str_replace('--->', '-->', $mermResp); // Fix triple dash $mermResp = str_replace('-- >', '-->', $mermResp); // Fix spaced arrow $mermResp = preg_replace('/[éèêë]/', 'e', $mermResp); // Strip accents e $mermResp = preg_replace('/[àâä]/', 'a', $mermResp); // Strip accents a $mermResp = preg_replace('/[ùûü]/', 'u', $mermResp); // Strip accents u $mermResp = preg_replace('/[ôö]/', 'o', $mermResp); // Strip accents o $mermResp = preg_replace('/[îï]/', 'i', $mermResp); // Strip accents i $mermResp = str_replace('ç', 'c', $mermResp); // Strip ç $mermResp = preg_replace('/\r\n?/', "\n", $mermResp); // Normalize line endings $mermResp = preg_replace('/flowchart TD/i', 'graph LR', $mermResp); // Force LR $mermResp = preg_replace('/flowchart LR/i', 'graph LR', $mermResp); // Normalize $mermResp = preg_replace('/graph TD/i', 'graph LR', $mermResp); // Force LR if(preg_match('/^(flowchart|graph)\s/i', $mermResp)) { $mermaidCode = $mermResp; error_log("WEVIA_MERM: Sovereign mermaid OK len=".strlen($mermaidCode)." connections=".substr_count($mermaidCode, "-->")); // Quality gate: reject too-simple schemas if(substr_count($mermaidCode, "-->") < 6) { error_log("WEVIA_MERM: REJECTED too simple (".substr_count($mermaidCode, "-->")." connections), retrying with fallback"); $mermaidCode = null; // Force fallback } file_put_contents($mermLog, date("H:i:s")." GROQ_OK len=".strlen($mermaidCode)." code=".substr($mermaidCode,0,100)."\n", FILE_APPEND); } } } if(!isset($mermaidCode)) { // Fallback: extract from text $isIshikawa = preg_match('/(ishikawa|cause|ar[eê]te|poisson|5M|6M)/i', $msg); if($isIshikawa) { // Build Ishikawa from text categories $mermaidCode = "flowchart LR\n P[Problème] --- M1[Méthodes]\n P --- M2[Milieu]\n P --- M3[Matériel]\n P --- M4[Main-oeuvre]\n P --- M5[Matière]\n P --- M6[Mesure]"; // Try to extract real categories from LLM response preg_match_all('/(?:^|\n)\s*([A-ZÀ-Ü][a-zà-ü\s\/]+)\s*(?:\n|:)/m', $resp, $cats); if(!empty($cats[1]) && count($cats[1]) >= 3) { $nodes = []; $i = 1; foreach(array_slice($cats[1], 0, 8) as $cat) { $cat = trim($cat); if(strlen($cat) > 2 && strlen($cat) < 40) { $nodes[] = " P --- M{$i}[" . str_replace(["[","]","(",")"], "", $cat) . "]"; $i++; } } if(count($nodes) >= 3) { $mermaidCode = "flowchart LR\n P[Problème]\n" . implode("\n", $nodes); } } } else { // Build flowchart from text structure preg_match_all('/(?:^|\n)\s*(?:\d+[\.\)]\s*)?([A-ZÀ-Ü][^:\n]{3,40})\s*(?::|$)/m', $resp, $steps); if(!empty($steps[1]) && count($steps[1]) >= 3) { $nodes = ["flowchart TD"]; $prev = null; $i = 1; foreach(array_slice($steps[1], 0, 10) as $step) { $step = trim(str_replace(["[","]","(",")","\"","'"], "", $step)); if(strlen($step) < 3 || strlen($step) > 35) continue; $id = "N{$i}"; $nodes[] = " {$id}[{$step}]"; if($prev) $nodes[] = " {$prev} --> {$id}"; $prev = $id; $i++; } if(count($nodes) >= 5) { $mermaidCode = implode("\n", $nodes); } } } } // end if(!isset($mermaidCode)) fallback if(isset($mermaidCode)) { // Render via LOCAL mmdc (sovereign) $cleanMerm = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $mermaidCode) ?: $mermaidCode; $hash = md5($cleanMerm . microtime()); $mmdFile = "/tmp/merm_{$hash}.mmd"; $pngFile = "/var/www/weval/wevia-ia/downloads/merm_{$hash}.png"; // Sanitize mermaid syntax (fix Groq/LLM errors — v3 2026-03-02) // 1. Arrow syntax fixes $cleanMerm = str_replace('|> ', '| ', $cleanMerm); $cleanMerm = preg_replace('#-->\|([^|]+)\|>\s*([A-Za-z])#', '-->|$1| $2', $cleanMerm); $cleanMerm = preg_replace('#-->\|([^|]+)\|>#', '-->|$1|', $cleanMerm); $cleanMerm = preg_replace('#\|>\s*([A-Za-z])#', '| $1', $cleanMerm); $cleanMerm = str_replace('==>', '-->', $cleanMerm); // thick arrows unsupported in some themes $cleanMerm = preg_replace('#-->\|([^|\n]*)$#m', '--> ', $cleanMerm); // unclosed -->|text // 2. Curly braces, long labels $cleanMerm = str_replace(['{', '}'], '', $cleanMerm); $cleanMerm = preg_replace('#-->\|([^|]{50,})\|#', '-->|...|',$cleanMerm); // 3. French accents → ASCII (proper UTF-8) $cleanMerm = strtr($cleanMerm, [ 'é'=>'e','è'=>'e','ê'=>'e','ë'=>'e','É'=>'E','È'=>'E','Ê'=>'E', 'à'=>'a','â'=>'a','ä'=>'a','À'=>'A','Â'=>'A', 'ù'=>'u','û'=>'u','ü'=>'u','Ù'=>'U','Û'=>'U', 'ô'=>'o','ö'=>'o','Ô'=>'O', 'î'=>'i','ï'=>'i','Î'=>'I', 'ç'=>'c','Ç'=>'C','œ'=>'oe','Œ'=>'OE', ]); $cleanMerm = strtr($cleanMerm, [ "é"=>"e","è"=>"e","ê"=>"e","ë"=>"e", "à"=>"a","â"=>"a","ä"=>"a", "ù"=>"u","û"=>"u","ü"=>"u", "ô"=>"o","ö"=>"o", "î"=>"i","ï"=>"i", "ç"=>"c","œ"=>"oe", ]); $cleanMerm = preg_replace('#^\s*(style|linkStyle|classDef|class)\s+.*$#m', '', $cleanMerm); $cleanMerm = preg_replace('#^\s*%%.*$#m', '', $cleanMerm); $cleanMerm = str_replace(['"', "'"], '', $cleanMerm); $cleanMerm = str_replace('**', '', $cleanMerm); // strip **bold** markers @file_put_contents("/var/www/weval/wevia-ia/downloads/merm_debug.txt", date("H:i:s")." SANITIZED: ".substr($cleanMerm,0,200)."\n", FILE_APPEND); file_put_contents($mmdFile, $cleanMerm); exec("/usr/bin/mmdc -i " . escapeshellarg($mmdFile) . " -o " . escapeshellarg($pngFile) . " -w 1400 -c /var/www/weval/wevia-ia/.mermaidrc -p /var/www/weval/wevia-ia/puppeteer.json 2>&1", $mOut, $mRet); @unlink($mmdFile); if(file_exists($pngFile) && filesize($pngFile) > 500) { // REPLACE prose with clean diagram $schemaTitle = mb_substr($msg, 0, 80); // Smart: if user asked for analysis+schema, keep text + append PNG $isMixedRequest = (mb_strlen($msg) > 50 || preg_match("/(business.?case|analyse|rapport|produis|compare|explique.*schema|avec.*schema)/i", $msg)); if($isMixedRequest && strlen($origResp) > 500) { // MIXED REQUEST: Keep the rich business case + append schema $cleanResp = preg_replace("/```mermaid.*?```/s", "", $origResp); $cleanResp = preg_replace("/```\n?graph .*?```/s", "", $cleanResp); $cleanResp = preg_replace("/!\[.*?\]\(.*?\)/", "", $cleanResp); // Remove fake images $cleanResp = trim($cleanResp); $resp = $cleanResp . "\n\n![Diagramme](/wevia-ia/downloads/merm_{$hash}.png)"; error_log("WEVIA_MERM: MIXED mode - kept ".strlen($cleanResp)." chars of business case + appended PNG"); } else { $resp = "![Diagramme](/wevia-ia/downloads/merm_{$hash}.png)"; } $mermaidRendered = true; @unlink($lockFile); // Release VRAM semaphore file_put_contents("/tmp/wevia_debug.log", date("H:i:s")." MERM_HANDLER isMixed=".(isset($isMixedRequest)?$isMixedRequest:"N/A")." respLen=".strlen($resp)." ", FILE_APPEND); error_log("WEVIA_MERM: PNG rendered OK " . filesize($pngFile) . " bytes, resp replaced len=".strlen($resp)); file_put_contents($mermLog, date("H:i:s")." PNG_OK ".filesize($pngFile)." bytes\n", FILE_APPEND); } else { error_log("WEVIA_MERM: mmdc FAILED ret=$mRet output=".implode(" ", $mOut)); file_put_contents($mermLog, date("H:i:s")." MMDC_FAIL ret=$mRet out=".implode(" ", $mOut)."\n", FILE_APPEND); $resp = mb_substr($resp, 0, 300) . "\n\n> Diagramme non disponible - reformulez votre demande."; // Fallback: keep a short summary, strip long prose $resp = mb_substr($resp, 0, 500) . "\n\n> Diagramme non disponible temporairement."; } } } // MERMAID -> IMAGE: catch ALL mermaid blocks (any format the LLM uses) $resp = preg_replace_callback('/```\s*mermaid\s*([^`]+)```/si', function($mm) { $code = trim($mm[1]); // Strip style/linkStyle/classDef/class lines $code = preg_replace('/^\s*(style|linkStyle|classDef|class)\s+.*$/m', '', $code); // Strip %% comments and ==== separators $code = preg_replace('/^\s*%%.*$/m', '', $code); // Strip subgraph/end blocks (flatten) // Keep subgraphs but sanitize labels (remove special chars that crash mmdc) $code = preg_replace('/subgraph\s+([^\[\n]+)\[([^\]]+)\]/', 'subgraph $2', $code); $code = preg_replace('/^\s*end\s*$/m', '', $code); // Clean labels: remove problematic chars // Truncate long labels $code = preg_replace_callback('/\[([^\]]+)\]/', function($lb) { $t = $lb[1]; // Remove content in parentheses $t = preg_replace('/\s*\([^)]*\)/', '', $t); // Replace slashes with - $t = str_replace(['/', '\\'], ' - ', $t); // Remove quotes and special chars $t = str_replace(['"', "'", '&', '<', '>', '{', '}'], '', $t); // Truncate to 40 chars if (mb_strlen($t) > 40) $t = mb_substr($t, 0, 37) . '...'; return '[' . trim($t) . ']'; }, $code); // Limit to max 15 lines (nodes+connections) $lines = explode("\n", $code); if (count($lines) > 16) { $code = implode("\n", array_slice($lines, 0, 16)); } // Collapse blank lines $code = preg_replace('/\n{2,}/', "\n", trim($code)); if (strlen($code) < 10) return ''; // Sanitize mermaid syntax (fix LLM errors — v3 2026-03-02) // 1. Arrow syntax fixes $code = str_replace('|> ', '| ', $code); $code = preg_replace('#-->\|([^|]+)\|>\s*([A-Za-z])#', '-->|$1| $2', $code); $code = preg_replace('#-->\|([^|]+)\|>#', '-->|$1|', $code); $code = preg_replace('#\|>\s*([A-Za-z])#', '| $1', $code); $code = str_replace('==>', '-->', $code); $code = preg_replace('#-->\|([^|\n]*)$#m', '--> ', $code); // 2. Curly braces, long labels $code = str_replace(['{', '}'], '', $code); $code = preg_replace('#-->\|([^|]{50,})\|#', '-->|...|',$code); // 3. French accents → ASCII $code = strtr($code, [ 'é'=>'e','è'=>'e','ê'=>'e','ë'=>'e','É'=>'E','È'=>'E','Ê'=>'E', 'à'=>'a','â'=>'a','ä'=>'a','À'=>'A','Â'=>'A', 'ù'=>'u','û'=>'u','ü'=>'u','Ù'=>'U','Û'=>'U', 'ô'=>'o','ö'=>'o','Ô'=>'O', 'î'=>'i','ï'=>'i','Î'=>'I', 'ç'=>'c','Ç'=>'C','œ'=>'oe','Œ'=>'OE', ]); // 4. HTML entities + semicolons in labels $code = preg_replace('#&[a-z]+;#i', '', $code); $code = str_replace(';', ',', $code); // 5. ::: class syntax removal $code = preg_replace('#:::[\w]+#', '', $code); // 6. Bold markers + flowchart fix $code = str_replace('**', '', $code); $code = preg_replace('/flowchart\s+(TD|LR|TB|RL|BT)/i', 'graph $1', $code); // flowchart→graph compat // 7. Empty lines cleanup $code = preg_replace('/\n{3,}/', "\n", $code); @file_put_contents("/var/www/weval/wevia-ia/downloads/merm_debug.txt", date("H:i:s")." HANDLER2 code=".substr($code,0,200)."\n", FILE_APPEND); // Local render with mmdc $hash = md5($code); $mmdFile = "/tmp/merm_{$hash}.mmd"; $pngFile = "/var/www/weval/wevia-ia/downloads/merm_{$hash}.png"; if (!file_exists($pngFile) || filesize($pngFile) < 500) { file_put_contents($mmdFile, $code); $cmd = "/usr/bin/mmdc -i " . escapeshellarg($mmdFile) . " -o " . escapeshellarg($pngFile) . " -w 1400 -c /var/www/weval/wevia-ia/.mermaidrc -p /var/www/weval/wevia-ia/puppeteer.json 2>&1"; exec($cmd, $out, $ret); @unlink($mmdFile); } if (file_exists($pngFile) && filesize($pngFile) > 500) { return '![Diagramme](/wevia-ia/downloads/merm_' . $hash . '.png)'; } // Fallback: text explanation (mermaid.ink often fails too) error_log("WEVIA_MERM: HANDLER2 mmdc failed, returning text fallback"); return '> 📊 *Diagramme en cours de preparation — veuillez reformuler votre demande pour un meilleur resultat.*'; }, $resp); // POST-CLEANUP: strip prose only for SIMPLE schema (not mixed business case+schema) $isMixedFinal = (mb_strlen($msg) > 50 || preg_match("/(business.?case|analyse|rapport|produis|compare|avec.*sch[eé]ma)/i", $msg)); if($isSchemaRequest && strpos($resp, '![Diagramme') !== false && !$isMixedFinal) { if($_imgM) $resp = $_imgM[0]; } // ═══ 8. LEARNING LOG ═══ logLearning($pdo, $msg, $resp, $usedProvider, $latency, !empty($webContext), !empty($kbContext), $intent, $verified); // ═══ 9. UPDATE EPISODIC MEMORY ═══ updateMemory($pdo, $ses, $msg, $resp, $intent); autoPopulateMemcells($pdo, $ses, $msg, $resp, $intent); // Save response $pdo->prepare("INSERT INTO messages (conversation_id,role,content) VALUES (?,?,?)")->execute([$cid,"assistant",$resp]); // ═══ PUBLIC RESPONSE SANITIZER ═══ // Strip [IMAGE]...[/IMAGE] blocks (raw matplotlib code) $resp = preg_replace('/\[IMAGE\].*?\[\/IMAGE\]/s', '', $resp); // Strip any remaining raw matplotlib/numpy code blocks $resp = preg_replace('/```(?:python)?\s*\n(?:import matplotlib|from matplotlib|import numpy).*?```/s', '', $resp); // Strip orphaned [/IMAGE] tags $resp = str_replace('[/IMAGE]', '', $resp); // Strip "import matplotlib..." lines not in code blocks $resp = preg_replace('/^(?:import matplotlib|from matplotlib|import numpy|img = imread|plt\.\w+|pdf\.image).*$/m', '', $resp); // ═══ NEURO-SYMBOLIC VERIFICATION ═══ $rulesWarning = applyBusinessRules($intent, $msg, $resp); // NOTE: rulesWarning stored in metadata only, NOT appended to user response // Prevents internal engine labels from leaking to end users $resp = sanitizePublicResponse($resp); // === REQUEST LOG === @file_put_contents("/var/log/wevia-requests.log", date("Y-m-d H:i:s") . " | " . ($usedProvider ?? "none") . " | " . $latency . "ms | " . strlen($resp ?? "") . "c | " . $mode . " | " . substr($msg, 0, 50) . "\n", FILE_APPEND | LOCK_EX); die(json_encode([ "response" => $resp, "conversation_id" => $cid, "provider" => "⚡ " . $latency . "ms", "mode" => $mode, "latency_ms" => $latency, "sources" => $sources, "used_kb" => !empty($kbContext), "rules_warning" => $rulesWarning ?? null, "used_web" => !empty($webContext), "used_vision" => $hasImage, "used_graph" => !empty($graphContext), "used_memory" => !empty($memoryContext), "verified" => $verified, "critic_note" => $criticNote, "intent" => $intent, "hallucination_flags" => $GLOBALS["_hallucinationFlags"] ?? [], "cognitive_flags" => $GLOBALS["_cogFlags"] ?? [], // "_pre_sanitize" removed for production // "_post_sanitize" removed for production "engine" => "WEVIA IA" ])); } // ═══════════════════════════════════════════════════════════ // SMART ROUTER // ═══════════════════════════════════════════════════════════ // ═══ YOUTUBE AUTO-EXTRACT — Detect YouTube URLs and get transcript ═══ function autoYouTube($msg) { if (!preg_match('/(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/', $msg, $m)) return ""; $videoId = $m[2]; try { $ctx = stream_context_create(['http'=>['method'=>'GET','timeout'=>8]]); $resp = @file_get_contents("http://127.0.0.1:5880/wevia-ia/wevia-youtube.php?url=https://youtube.com/watch?v={$videoId}", false, $ctx); if (!$resp) return ""; $d = @json_decode($resp, true); if (!$d || !isset($d['transcript'])) return ""; $transcript = substr($d['transcript'], 0, 3000); // Cap at 3000 chars return "\n\n## TRANSCRIPTION YOUTUBE (vidéo: {$videoId})\n{$transcript}\n"; } catch (Exception $e) { return ""; } } // ═══ DARK MODULES ENRICHMENT — Connect all 7 remaining modules ═══ function darkEnrichment($msg, $intent, $pdo) { $enrichment = ""; // ── SHADOW-RAG: Priority intelligence search (always, for non-greetings) ── try { $ctx = stream_context_create(['http'=>['method'=>'POST','header'=>'Content-Type: application/json', 'content'=>json_encode(['action'=>'search','query'=>substr($msg,0,200),'limit'=>3]),'timeout'=>4]]); $sr = @file_get_contents("http://127.0.0.1:5880/api/shadow-rag.php", false, $ctx); if ($sr) { $d = @json_decode($sr, true); if ($d && !empty($d['results'])) { $parts = []; foreach (array_slice($d['results'], 0, 3) as $r) { $parts[] = "[{$r['category']}] " . substr($r['content'] ?? $r['title'] ?? '', 0, 300); } if ($parts) $enrichment .= "\n[SHADOW-RAG] " . implode("\n", $parts) . "\n"; } } } catch (Exception $e) {} // ── DARK-SCRAPER: Auto-scrape when user pastes a non-YouTube URL ── if (preg_match('/https?:\/\/[^\s]+/i', $msg, $urlMatch)) { $url = $urlMatch[0]; // Skip YouTube (handled separately), skip internal if (!preg_match('/youtube|youtu\.be/i', $url) && !preg_match('/weval-consulting|localhost|127\.0/i', $url)) { try { $ctx = stream_context_create(['http'=>['method'=>'POST','header'=>'Content-Type: application/json', 'content'=>json_encode(['action'=>'scrape','url'=>$url]),'timeout'=>8]]); $sr = @file_get_contents("http://127.0.0.1:5880/api/dark-scraper.php", false, $ctx); if ($sr) { $d = @json_decode($sr, true); if ($d && isset($d['text'])) { $text = substr($d['text'], 0, 2000); $title = $d['title'] ?? $url; $enrichment .= "\n[URL-SCRAPE:{$title}] {$text}\n"; } } } catch (Exception $e) {} } } // ── IA-DISCOVERY: When user asks about AI tools/comparisons ── if (preg_match('/\b(ia|intelligence.?artif|llm|gpt|claude|gemini|deepseek|mistral|chatgpt|copilot|compare.*ia|meilleur.*ia)\b/i', $msg)) { try { $ctx = stream_context_create(['http'=>['method'=>'POST','header'=>'Content-Type: application/json', 'content'=>json_encode(['action'=>'search','query'=>substr($msg,0,200)]),'timeout'=>4]]); $sr = @file_get_contents("http://127.0.0.1:5880/api/ia-discovery.php", false, $ctx); if ($sr) { $d = @json_decode($sr, true); if ($d && !empty($d['results'])) { $parts = []; foreach (array_slice($d['results'], 0, 3) as $r) { $parts[] = ($r['name'] ?? '') . ": " . substr($r['description'] ?? $r['content'] ?? '', 0, 200); } if ($parts) $enrichment .= "\n[IA-DISCOVERY] " . implode("\n", $parts) . "\n"; } } } catch (Exception $e) {} } // ── DARK-MATRIX: Deep company intel (when domain detected, supplements dark-scout) ── if (preg_match_all('/\b([a-zA-Z0-9-]+\.(com|org|net|io|ai|co|ma|fr))\b/i', $msg, $dm)) { $domains = array_unique(array_slice($dm[0], 0, 2)); foreach ($domains as $domain) { // Skip common domains if (preg_match('/google|youtube|facebook|twitter|wikipedia|github/i', $domain)) continue; try { $ctx = stream_context_create(['http'=>['method'=>'POST','header'=>'Content-Type: application/json', 'content'=>json_encode(['target'=>$domain,'depth'=>1,'mode'=>'fast']),'timeout'=>6]]); $sr = @file_get_contents("http://127.0.0.1:5880/api/dark-matrix.php", false, $ctx); if ($sr) { $d = @json_decode($sr, true); if ($d && isset($d['status']) && $d['status'] === 'completed') { $enrichment .= "\n[DARK-MATRIX:{$domain}] " . substr(json_encode($d['summary'] ?? $d['data'] ?? []), 0, 500) . "\n"; } } } catch (Exception $e) {} } } // ── LINKEDIN: When user mentions LinkedIn profiles/companies ── if (preg_match('/\b(linkedin|profil|profile|recrutement|hiring|recrut|réseau.?pro|networking)\b/i', $msg) || preg_match('/linkedin\.com/', $msg)) { $liQuery = preg_replace('/\b(linkedin|profil|cherche|trouve|show|montre)\b/i', '', $msg); $liQuery = trim(preg_replace('/\s+/', ' ', $liQuery)); if (mb_strlen($liQuery) >= 3) { try { $ctx = stream_context_create(['http'=>['method'=>'POST','header'=>'Content-Type: application/json', 'content'=>json_encode(['action'=>'search','query'=>substr($liQuery,0,200),'type'=>'auto']),'timeout'=>10]]); $sr = @file_get_contents("http://127.0.0.1:5880/wevia-ia/wevia-linkedin.php", false, $ctx); if ($sr) { $d = @json_decode($sr, true); if ($d && !empty($d['results'])) { $parts = []; foreach (array_slice($d['results'], 0, 3) as $r) { $info = ($r['name'] ?? $r['company_name'] ?? $r['title'] ?? ''); $headline = $r['headline'] ?? $r['snippet'] ?? ''; $url = $r['url'] ?? ''; $parts[] = "{$info} — {$headline} ({$url})"; } if ($parts) $enrichment .= "\n[LINKEDIN] " . implode("\n", $parts) . "\n"; } } } catch (Exception $e) {} } } return $enrichment; } // ═══ AUTO-ARMAMENT — Dark Intelligence Enrichment ═══ function autoArmament($msg, $pdo) { $enrichment = ""; // ── CYBER SECURITY AUTO-SCAN: trigger on security questions ── if (preg_match('/\b(scan|audit|s[eé]curit[eé]|vuln[eé]rabilit|pentest|hack|cyber|firewall|intrusion|malware|ransomware|zero.?day|CVE|OWASP|hardening|WEVIA Code Security)\b/i', $msg)) { try { $ctx = stream_context_create(['http'=>['method'=>'POST','header'=>'Content-Type: application/json', 'content'=>json_encode(['action'=>'quick_scan']),'timeout'=>10]]); $sr = @file_get_contents("http://127.0.0.1:5880/wevia-ia/wevia-cyber-scan.php", false, $ctx); if ($sr) { $scan = @json_decode($sr, true); if ($scan && isset($scan['score'])) { $enrichment .= "\n[CYBER-SCAN] Score: {$scan['score']}/100 Grade: {$scan['grade']} | Checks: {$scan['summary']['total_checks']} Pass: {$scan['summary']['passed']} Warn: {$scan['summary']['warnings']} Fail: {$scan['summary']['failures']}\n"; foreach ($scan['findings'] as $f) { if ($f['status'] === 'FAIL' || $f['status'] === 'WARN') { $enrichment .= " [{$f['severity']}] {$f['name']}: {$f['detail']}\n"; } } } } } catch (Exception $e) {} } $domains = []; if (preg_match_all('/\b([a-zA-Z0-9-]+\.(com|org|net|io|ai|co|ma|fr|de|uk))\b/i', $msg, $dm)) { $domains = array_unique($dm[0]); } $companyMap = ['deloitte'=>'deloitte.com','mckinsey'=>'mckinsey.com','bcg'=>'bcg.com','bain'=>'bain.com','pwc'=>'pwc.com','kpmg'=>'kpmg.com','accenture'=>'accenture.com','capgemini'=>'capgemini.com','ey'=>'ey.com','oracle'=>'oracle.com','salesforce'=>'salesforce.com','sap'=>'sap.com']; if (preg_match_all('/\b(' . implode('|', array_keys($companyMap)) . ')\b/i', $msg, $cm)) { foreach (array_unique(array_map('strtolower', $cm[0])) as $co) { if (isset($companyMap[$co]) && !in_array($companyMap[$co], $domains)) $domains[] = $companyMap[$co]; } } foreach (array_slice($domains, 0, 2) as $domain) { try { $stmt = $pdo->prepare("SELECT title, LEFT(content,300) as excerpt, priority FROM admin.shadow_knowledge WHERE source_domain=? ORDER BY priority DESC LIMIT 1"); $stmt->execute([$domain]); $cached = $stmt->fetch(PDO::FETCH_ASSOC); if ($cached) { $enrichment .= "\n[INTEL:{$domain}] {$cached['excerpt']}\n"; continue; } } catch (Exception $e) {} $ctx = stream_context_create(['http'=>['method'=>'POST','header'=>'Content-Type: application/json','content'=>json_encode(['target'=>$domain]),'timeout'=>5]]); $sr = @file_get_contents("http://127.0.0.1:5880/api/dark-scout.php", false, $ctx); if ($sr) { $s = @json_decode($sr, true); if ($s && isset($s['security_score'])) { $tech = implode(',', array_column($s['modules']['tech_stack'] ?? [], 'value')); $summary = "Score:{$s['security_score']}/100 Risk:{$s['risk_level']} Tech:[{$tech}]"; $enrichment .= "\n[DARK-SCOUT:{$domain}] {$summary}\n"; try { $pdo->prepare("INSERT INTO admin.shadow_knowledge (title,content,category,classification,priority,source,source_domain) VALUES (?,?,'technology','auto_scout',0.7,'auto_armament',?) ON CONFLICT DO NOTHING")->execute(["Auto-Scout:{$domain}",$summary,$domain]); } catch (Exception $e) {} } } } return $enrichment; } function smartRoute($msg, $mode, $intent) { // ═══ SMART ROUTE V2 — Cloud FAST + GPU when available ═══ // Check GPU availability (fast 2s check, cached 60s) static $gpuOk = null; static $gpuCheckTime = 0; if($gpuOk === null || (time() - $gpuCheckTime) > 60) { $ch = curl_init("http://127.0.0.1:11434/api/tags"); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CONNECTTIMEOUT=>5]); $r = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $gpuOk = ($code === 200 && strlen($r) > 10); $gpuCheckTime = time(); } // CLOUD-FIRST: GPU via tunnel too slow (20s). Cloud = 1-3s // GPU will become local after S88 migration (Claude 1 in progress) // if($gpuOk) { /* DISABLED: tunnel latency kills UX */ } // GPU unavailable → Cloud routing if($intent === "greeting" || $mode === "fast") return "groq"; if($intent === "code" || $intent === "schema") return "cerebras"; if(in_array($intent, ["consulting","analysis","compliance","reasoning"])) return "sambanova"; return "groq"; // Greetings if($intent === "greeting") return "deepseek_gpu"; // Fast mode → GPU 8b if($mode === "fast") return "deepseek_gpu"; // Code/Schema → GPU qwen coder 14b (spécialisé code) if($intent === "code" || $intent === "schema") return "gpu_coder"; // ═══ DEEPSEEK R1:32b = FRONTAL SOUVERAIN pour tout sauf greetings/code ═══ // Consulting/Analysis/General/Creative → GPU DeepSeek R1:32b (le plus puissant) if(in_array($intent, ["consulting","analysis","compliance","general","creative","translation","search"])) { return "gpu_reason"; } // ISP/Server/Technique → GPU DeepSeek R1:32b (raisonnement profond) if(in_array($intent, ["isp","server","medical","math"])) { return "gpu_reason"; } // Verified mode → GPU DeepSeek R1:32b if($mode === "verified") return "gpu_reason"; // Default: DeepSeek R1:32b pour messages longs, GPU 8b pour courts if(mb_strlen($msg) > 60) return "gpu_reason"; return "deepseek_gpu"; } function detectIntent($msg) { $l = mb_strtolower($msg); if(preg_match('/(code|debug|script|python|php|bash|sql|function|class|regex|json|xml|html|css|javascript|api)/i', $l)) return "code"; if(preg_match('/(calcul|math|équation|statistiq|probabilit|formule|intégr|dérivé)/i', $l)) return "math"; if(preg_match('/(analyse|stratégie|rapport|audit|compare|évalue|swot|roadmap|planifi)/i', $l)) return "analysis"; if(preg_match('/(tradui|translate|traduction)/i', $l)) return "translation"; if(preg_match('/(sch[eé]ma|schema|diagram|svg|html|dashboard|artifact|mermaid|flowchart|organigramme|ishikawa|fonctionnement|graphique.*cause|cause.*effet|graph.*cause|5m|6m|arête|poisson)/i', $l)) return "creative"; if(preg_match('/(recherche|cherche|actualit|news|google|web)/i', $l)) return "search"; // INTENT CLASSIFIERS: Image, PDF, Schema (before greeting catch-all) // GREETING FIRST — avoid classifying "Bonjour" as PDF if(preg_match('/^(bonjour|bonsoir|salut|salam|hello|hey|hi|yo|coucou|bjr|bsr|merci|thanks|thank|ok|oui|non|nope|yep|bye|au revoir|a bientot|bonne nuit|bonne journee|ca va|comment vas|aide|help|d accord|parfait|genial|super|cool|nice|bravo|top|wesh|hola|guten|ciao|ahlan)[\s!?.]*$/i', $l)) return "greeting"; if(preg_match('/(image|photo|genere.*image|genre.*image|dessine|illustr|portrait|logo|visuel|genere.*photo|genre.*photo)/i', $l)) return "image"; if(preg_match('/(pdf|document|rapport|export.*pdf|genere.*pdf|genre.*pdf|telecharge)/i', $l)) return "pdf"; if(preg_match('/(schema|shema|diagramme|organigramme|flowchart|architecture|fonctionnement|pipeline|workflow|directeur|roadmap|cartographie|mapping|mermaid|ishikawa)/i', $l)) return "schema"; if(preg_match('/(bonjour|bonsoir|salut|salam|hello|hey|hi|yo|coucou|bjr|bsr|merci|thanks|thank|ok|oui|non|nope|yep|bye|au revoir|a bientot|bonne nuit|bonne journee|ca va|comment vas|aide|help|d accord|parfait|genial|super|cool|nice|bravo|top|wesh|hola|guten|ciao|ahlan)/i', $l)) return "greeting"; if(preg_match('/(sap|erp|s4hana|oracle|consultant|pwc|deloitte|kpmg)/i', $l)) return "consulting"; if(preg_match('/(isp|inbox|deliverability|warmup|dkim|spf|dmarc|spam|bounce|email.*send)/i', $l)) return "isp"; if(preg_match('/(serveur|server|ssh|docker|disk|ram|cpu|postgresql|nginx)/i', $l)) return "server"; if(preg_match('/(conseil|stratégi|recommand|benchmark|diagnostic|audit|évaluation|due.diligence|feuille.de.route|business.case|consulting)/i', $l)) return "consulting"; if(preg_match('/(analyse|comparai|SWOT|impact|évaluer|assessment|comparer|versus|vs)/i', $l)) return "analysis"; if(preg_match('/(conformit|compliance|RGPD|GDPR|NIS2|PCI|ISO.27001|FDA|GMP|réglementai)/i', $l)) return "compliance"; if(preg_match('/(médic|pharma|santé|patient|cliniq|thérapeutiq|médicament)/i', $l)) return "medical"; return "general"; } // ═══════════════════════════════════════════════════════════ // KB SEARCH — ALL 10 SOURCES // ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════ // NEURO-SYMBOLIC ENGINE — Business Rules Verification // ═══════════════════════════════════════════════════════════ function applyBusinessRules($intent, $msg, $resp) { $rules = []; $warnings = []; $l = mb_strtolower($msg . " " . $resp); // SAP RULES if(preg_match('/sap|s\/4hana|abap|fiori/i', $l)) { $rules[] = "SAP"; if(preg_match('/migration|migrer/i', $l) && !preg_match('/brownfield|greenfield|selective/i', $resp)) $warnings[] = "⚠️ Migration SAP mentionnée sans préciser l'approche (brownfield/greenfield/selective)"; if(preg_match('/rise.*with.*sap/i', $l) && !preg_match('/BTP|clean.*core/i', $resp)) $warnings[] = "⚠️ RISE with SAP sans mention de BTP/Clean Core"; } // PHARMA RULES if(preg_match('/pharma|gmp|fda|amm|médicament/i', $l)) { $rules[] = "PHARMA"; if(preg_match('/données.*patient|patient.*data/i', $l) && !preg_match('/RGPD|GDPR|consentement|anonymis/i', $resp)) $warnings[] = "⚠️ Données patients mentionnées sans rappel RGPD/consentement"; if(preg_match('/fabrication|manufacturing|lot|batch/i', $l) && !preg_match('/GMP|BPF|traçabilit/i', $resp)) $warnings[] = "⚠️ Fabrication pharma sans mention GMP/BPF/traçabilité"; } // COMPLIANCE RULES if(preg_match('/conformit|compliance|audit|réglementai/i', $l)) { $rules[] = "COMPLIANCE"; if(preg_match('/maroc/i', $l) && !preg_match('/CNDP|loi.*09.08|AMMC/i', $resp)) $warnings[] = "⚠️ Compliance Maroc sans mention CNDP/Loi 09-08"; if(preg_match('/europe|UE|EU/i', $l) && !preg_match('/NIS2|DORA|RGPD/i', $resp)) $warnings[] = "⚠️ Compliance EU sans mention NIS2/DORA/RGPD"; } // FINANCE RULES if(preg_match('/banque|bank|fintech|paiement|payment/i', $l)) { $rules[] = "FINANCE"; if(!preg_match('/PCI.DSS|PSD2|BAM|Bank.*Al.*Maghrib/i', $resp)) $warnings[] = "⚠️ Secteur bancaire sans mention réglementaire (PCI-DSS/PSD2/BAM)"; } // CYBERSECURITY RULES if(preg_match('/cybersécurité|zero.*trust|soc|siem/i', $l)) { $rules[] = "CYBER"; if(preg_match('/zero.*trust/i', $l) && !preg_match('/micro.?segment|least.*privilege|MFA/i', $resp)) $warnings[] = "⚠️ Zero Trust sans les piliers fondamentaux (micro-segmentation, least privilege, MFA)"; } if(empty($warnings)) return null; return "🔒 Moteur neuro-symbolique [" . implode("+", $rules) . "] :\n" . implode("\n", $warnings); } function searchAllKB($pdo, $msg, $intent, &$kbSources = []) { try { $words = preg_split('/\s+/', mb_strtolower(trim($msg))); // French stopwords filter — prevents common words from diluting KB search $stopwords = ["les","des","une","que","qui","dans","pour","avec","sur","par","est","sont","pas","mais","plus","tout","tous","cette","ces","aux","son","ses","nos","vos","leur","leurs","ont","ete","quel","quels","quelle","quelles","comment","quoi","dont","comme","faire","fait","peut","faut","aussi","entre","autre","autres","meme","apres","avant","depuis","encore","tres","bien","sans","sous","chez","vers","lors","car","donc","soit","chaque","peu","trop","assez","ici","puis"]; $terms = array_values(array_filter($words, fn($w) => mb_strlen($w) >= 3 && !in_array($w, $stopwords))); // Sort by length DESC — longer terms are more specific usort($terms, fn($a, $b) => mb_strlen($b) - mb_strlen($a)); if(empty($terms)) return ""; $kbParts = []; $terms5 = array_slice($terms, 0, 5); $terms3 = array_slice($terms, 0, 3); // Helper: build LIKE WHERE clause $buildLike = function($fields, $terms, $prefix = "") { $parts = []; $params = []; foreach($terms as $i => $t) { $sub = []; foreach($fields as $f) { $key = ":{$prefix}{$f}{$i}"; $sub[] = "LOWER({$f}) LIKE {$key}"; $params[$key] = "%{$t}%"; } $parts[] = "(" . implode(" OR ", $sub) . ")"; } return [implode(" OR ", $parts), $params]; }; // ── 1. knowledge_base ── try { [$where, $params] = $buildLike(["title","content"], $terms5, "kb"); $st = $pdo->prepare("SELECT title, category, LEFT(content, 2500) as excerpt FROM knowledge_base WHERE ({$where}) AND category NOT IN ('pmta','brain','adx','seed','email','isp','server') ORDER BY id DESC LIMIT 5"); $st->execute($params); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $kbParts[] = "📚 **{$r['title']}** ({$r['category']})\n{$r['excerpt']}"; } } catch(Exception $e) {} // ── 2. commonia_knowledge ── try { [$where, $params] = $buildLike(["question","answer"], $terms3, "ck"); $st = $pdo->prepare("SELECT question, answer, category FROM commonia_knowledge WHERE {$where} LIMIT 3"); $st->execute($params); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $kbParts[] = "💡 **Q:** {$r['question']}\n**R:** " . substr($r['answer'], 0, 1500); } } catch(Exception $e) {} // ── 3. chatbot_knowledge ── try { [$where, $params] = $buildLike(["question","answer"], $terms3, "cb"); $st = $pdo->prepare("SELECT question, answer FROM chatbot_knowledge WHERE {$where} LIMIT 2"); $st->execute($params); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $kbParts[] = "🤖 **Q:** {$r['question']}\n**R:** " . substr($r['answer'], 0, 1000); } } catch(Exception $e) {} // ── 4. hamid_knowledge (topic/content) ── try { [$where, $params] = $buildLike(["topic","content"], $terms3, "hk"); $st = $pdo->prepare("SELECT topic, category, LEFT(content, 1500) as content FROM hamid_knowledge WHERE {$where} LIMIT 2"); $st->execute($params); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $kbParts[] = "🧠 **{$r['topic']}** ({$r['category']})\n{$r['content']}"; } } catch(Exception $e) {} // ── 5. sentinel_knowledge (topic/content) ── try { [$where, $params] = $buildLike(["topic","content"], $terms3, "sk"); $st = $pdo->prepare("SELECT topic, category, LEFT(content, 1000) as content FROM sentinel_knowledge WHERE {$where} LIMIT 2"); $st->execute($params); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $kbParts[] = "🛡️ **{$r['topic']}** ({$r['category']})\n{$r['content']}"; } } catch(Exception $e) {} // ── 6. wevia_knowledge (topic/content) ── try { [$where, $params] = $buildLike(["topic","content"], $terms3, "wk"); $st = $pdo->prepare("SELECT topic, category, LEFT(content, 1000) as content FROM wevia_knowledge WHERE {$where} LIMIT 2"); $st->execute($params); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $kbParts[] = "🌐 **{$r['topic']}** ({$r['category']})\n{$r['content']}"; } } catch(Exception $e) {} // ── 7. ISP knowledge ── if(preg_match('/\b(isp|gmail|yahoo|outlook|hotmail|aol|deliverability|inbox|spam|warmup|dkim|spf|dmarc|bounce)\b/i', implode(' ', $terms))) { try { $ispTerms = array_filter($terms, fn($t) => !in_array($t, ['isp','deliverability','email','inbox'])); if(!empty($ispTerms)) { $ispWhere = []; $ispParams = []; foreach(array_values($ispTerms) as $i => $t) { $ispWhere[] = "LOWER(isp) LIKE :isp{$i}"; $ispParams[":isp{$i}"] = "%{$t}%"; } $st = $pdo->prepare("SELECT isp, country, filter_name, filter_type, filter_bypass_method, optimal_volume_per_hour, optimal_volume_per_day, warmup_days_needed, best_send_hours, requires_dkim, requires_spf, requires_dmarc, current_difficulty, notes FROM isp_knowledge WHERE " . implode(" OR ", $ispWhere) . " LIMIT 3"); $st->execute($ispParams); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $kbParts[] = "📧 **ISP: {$r['isp']}** ({$r['country']})\nFiltre: {$r['filter_name']} ({$r['filter_type']})\nBypass: {$r['filter_bypass_method']}\nVolume: {$r['optimal_volume_per_hour']}/h, {$r['optimal_volume_per_day']}/jour\nWarmup: {$r['warmup_days_needed']}j | DKIM:{$r['requires_dkim']} SPF:{$r['requires_spf']} DMARC:{$r['requires_dmarc']}\nDifficulté: {$r['current_difficulty']}"; } } else { $st = $pdo->query("SELECT isp, country, current_difficulty, optimal_volume_per_day, warmup_days_needed FROM isp_knowledge ORDER BY current_difficulty DESC LIMIT 5"); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $kbParts[] = "📧 {$r['isp']} ({$r['country']}): diff={$r['current_difficulty']}, vol/j={$r['optimal_volume_per_day']}, warmup={$r['warmup_days_needed']}j"; } } } catch(Exception $e) {} } // ── 8. kb_documents ── try { [$where, $params] = $buildLike(["title","content"], $terms3, "kd"); $st = $pdo->prepare("SELECT title, LEFT(content, 1500) as content FROM kb_documents WHERE {$where} LIMIT 1"); $st->execute($params); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $kbParts[] = "📄 **Doc:** {$r['title']}\n{$r['content']}"; } } catch(Exception $e) {} // ── 9. brain_knowledge (key/value) ── try { [$where, $params] = $buildLike(["key","value"], $terms3, "bk"); $st = $pdo->prepare("SELECT key, value, category FROM brain_knowledge WHERE {$where} OR category IN ('nexus') LIMIT 5"); $st->execute($params); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $kbParts[] = "🎯 **{$r['key']}** ({$r['category']}): {$r['value']}"; } } catch(Exception $e) {} // ── 10. weval_mind_learning ── try { [$where, $params] = $buildLike(["input","output"], $terms3, "ml"); $st = $pdo->prepare("SELECT input, LEFT(output, 1000) as output FROM weval_mind_learning WHERE {$where} LIMIT 2"); $st->execute($params); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $kbParts[] = "🧪 **Appris:** {$r['input']}\n{$r['output']}"; } } catch(Exception $e) {} // ── 11. Local file KB (PRIORITY — runs before display) ── $fileKbParts = []; $stopwords = ['les','des','une','est','que','qui','par','pour','dans','sur','avec','son','ses','aux','pas','plus','ont','sont','fait','cette','tout','mais','comme','aussi','peut','sans','bien','entre','autre','encore','fait','elle','nous','vous','leur','quel','quoi','dont','etre','avoir','faire','comment']; $fileTerms = array_values(array_filter($terms, fn($t) => mb_strlen($t) >= 4 || !in_array($t, $stopwords))); $KB_INDEX = __DIR__ . '/kb-data/index.json'; if(!empty($fileTerms) && file_exists($KB_INDEX)) { try { $kbIndex = json_decode(file_get_contents($KB_INDEX), true) ?: []; $scored = []; foreach($kbIndex as $doc) { $sc = 0; $title = mb_strtolower($doc['title']??''); $hay = $title.' '.mb_strtolower(implode(' ',$doc['tags']??[]).' '.($doc['summary']??'').' '.($doc['category']??'').' '.($doc['filename']??'')); foreach($fileTerms as $t) { if(strpos($hay, $t) !== false) $sc++; if(strpos($title, $t) !== false) $sc += 3; // title boost } if($sc >= 1) { $scored[] = ['doc' => $doc, 'sc' => $sc]; } } // Sort by score DESC usort($scored, fn($a,$b) => $b['sc'] - $a['sc']); foreach(array_slice($scored, 0, 5) as $item) { $doc = $item['doc']; $filePath = __DIR__ . '/kb-data/' . $doc['id'] . '.txt'; $content = file_exists($filePath) ? file_get_contents($filePath) : ($doc['summary']??''); $fileKbParts[] = "\xf0\x9f\x93\x82 **" . $doc['title'] . "** [" . ($doc['ext']??'?') . "] (score:" . $item['sc'] . ")\n" . mb_substr($content, 0, 4000); } } catch(Exception $e) {} } // Prepend file KB results (highest priority) if(!empty($fileKbParts)) { $kbParts = array_merge($fileKbParts, $kbParts); } if(empty($kbParts)) return ""; foreach(array_slice($kbParts, 0, 5) as $kp) { $kbSources[] = "KB: " . substr(preg_replace('/[\n\r]/', ' ', $kp), 0, 80); } return implode("\n\n---\n\n", array_slice($kbParts, 0, 8)); } catch(Exception $e) { return ""; } } function searchGraph($pdo, $msg) { try { $terms = array_filter(preg_split('/\s+/', mb_strtolower($msg)), fn($w) => mb_strlen($w) >= 3); if(empty($terms)) return ""; // Find matching entities $likeParts = []; $params = []; foreach(array_slice($terms, 0, 3) as $i => $t) { $likeParts[] = "LOWER(m.entity) LIKE :ge{$i} OR LOWER(m.fact) LIKE :gf{$i}"; $params[":ge{$i}"] = "%{$t}%"; $params[":gf{$i}"] = "%{$t}%"; } // Get entities + their relations (1-hop graph traversal) $sql = " SELECT m.entity, m.fact, m.category, m.confidence, r.relation_type, m2.entity as related_entity, m2.fact as related_fact FROM admin.memcells m LEFT JOIN admin.memcell_relations r ON m.id = r.source_id LEFT JOIN admin.memcells m2 ON r.target_id = m2.id WHERE " . implode(" OR ", $likeParts) . " ORDER BY m.confidence DESC NULLS LAST LIMIT 5 "; $st = $pdo->prepare($sql); $st->execute($params); $rows = $st->fetchAll(PDO::FETCH_ASSOC); if(empty($rows)) return ""; $parts = []; foreach($rows as $r) { $line = "🔗 **{$r['entity']}** ({$r['category']}): {$r['fact']}"; if($r['related_entity']) { $line .= " → [{$r['relation_type']}] → {$r['related_entity']}: {$r['related_fact']}"; } $parts[] = $line; } return implode("\n", array_unique($parts)); } catch(Exception $e) { return ""; } } // ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════ // VECTOR EMBEDDINGS — nomic-embed-text on GPU // ═══════════════════════════════════════════════════════════ function generateEmbedding($text) { static $cache = []; $key = md5($text); if(isset($cache[$key])) return $cache[$key]; $ch = curl_init("http://127.0.0.1:11434/api/embeddings"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 8, CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_POSTFIELDS => json_encode(["model"=>"nomic-embed-text", "prompt"=>substr($text, 0, 2000)]) ]); $resp = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if($code !== 200) return null; $data = json_decode($resp, true); $vec = $data["embedding"] ?? null; if($vec && count($vec) === 768) { $cache[$key] = $vec; return $vec; } return null; } function vectorSearch($pdo, $session, $query, $limit = 5) { try { $vec = generateEmbedding($query); if(!$vec) return []; $vecStr = "[" . implode(",", $vec) . "]"; // Hybrid: session-first + cross-session $st = $pdo->prepare(" SELECT value as content, memory_type as content_type, session_id, 1 - (embedding <=> ?::vector) as similarity FROM admin.wevia_memory WHERE embedding IS NOT NULL AND 1 - (embedding <=> ?::vector) > 0.35 ORDER BY CASE WHEN session_id = ? THEN 0 ELSE 1 END, embedding <=> ?::vector LIMIT ? "); $st->execute([$vecStr, $vecStr, $session, $vecStr, $limit]); return $st->fetchAll(PDO::FETCH_ASSOC); } catch(Exception $e) { return []; } } function storeVector($pdo, $session, $content, $type, $memoryId = null) { try { $vec = generateEmbedding($content); if(!$vec) { return; } $vecStr = "[" . implode(",", $vec) . "]"; $pdo->prepare("INSERT INTO admin.memory_vectors (session_id, memory_id, content, content_type, embedding) VALUES (?, ?, ?, ?, ?::vector)") ->execute([$session, $memoryId, substr($content, 0, 1000), $type, $vecStr]); } catch(Exception $e) { } } // EPISODIC MEMORY — wevia_memory // ═══════════════════════════════════════════════════════════ function searchMemory($pdo, $session, $msg) { try { $terms = array_filter(preg_split('/\s+/', mb_strtolower($msg)), fn($w) => mb_strlen($w) >= 3); $parts = []; // === TIER 1: Identity & Preferences (always loaded) === $st = $pdo->prepare("SELECT key, value, memory_type FROM admin.wevia_memory WHERE session_id = ? AND memory_type IN ('identity','preference','decision','constraint') ORDER BY confidence DESC, access_count DESC LIMIT 5"); $st->execute([$session]); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $icons = ['identity'=>'\xF0\x9F\x91\xA4','preference'=>'\xF0\x9F\x92\xA1','decision'=>'\xE2\x9C\x85','constraint'=>'\xE2\x9A\xA0\xEF\xB8\x8F']; $icon = $icons[$r['memory_type']] ?? '\xF0\x9F\x92\xAD'; $parts[] = "{$icon} [{$r['memory_type']}] {$r['value']}"; } // === TIER 2: Session summaries === $st = $pdo->prepare("SELECT value FROM admin.wevia_memory WHERE session_id = ? AND memory_type = 'summary' ORDER BY created_at DESC LIMIT 2"); $st->execute([$session]); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $parts[] = "\xF0\x9F\x93\x8B {$r['value']}"; } // === TIER 3: Keyword-matched entities === if(!empty($terms)) { $likeParts = []; $params = [":sid" => $session]; foreach(array_slice($terms, 0, 4) as $i => $t) { $likeParts[] = "(LOWER(value) LIKE :mv{$i} OR LOWER(key) LIKE :mk{$i})"; $params[":mv{$i}"] = "%{$t}%"; $params[":mk{$i}"] = "%{$t}%"; } $st = $pdo->prepare("SELECT key, value, memory_type, confidence FROM admin.wevia_memory WHERE session_id = :sid AND (" . implode(" OR ", $likeParts) . ") AND memory_type NOT IN ('summary') ORDER BY confidence DESC, access_count DESC LIMIT 4"); $st->execute($params); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $parts[] = "\xF0\x9F\x94\x8D {$r['value']}"; } // Cross-session knowledge $params2 = []; $likeParts2 = []; foreach(array_slice($terms, 0, 3) as $i => $t) { $likeParts2[] = "(LOWER(value) LIKE :xv{$i})"; $params2[":xv{$i}"] = "%{$t}%"; } $params2[":xsid"] = $session; $st = $pdo->prepare("SELECT value FROM admin.wevia_memory WHERE session_id != :xsid AND memory_type IN ('entity','preference','identity','decision') AND (" . implode(" OR ", $likeParts2) . ") ORDER BY confidence DESC LIMIT 2"); $st->execute($params2); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $parts[] = "\xF0\x9F\x8C\x90 [cross-session] {$r['value']}"; } } // === TIER 4: Recent topics for continuity === $st = $pdo->prepare("SELECT value FROM admin.wevia_memory WHERE session_id = ? AND memory_type = 'topic' ORDER BY created_at DESC LIMIT 3"); $st->execute([$session]); $recentTopics = $st->fetchAll(PDO::FETCH_COLUMN); if(!empty($recentTopics)) { $parts[] = "\xF0\x9F\x95\x90 Sujets r\xC3\xA9cents: " . implode(" > ", array_map(fn($t) => substr($t, 0, 60), $recentTopics)); } // === TIER 5: SEMANTIC VECTOR SEARCH (GPU embeddings) === try { $vecResults = vectorSearch($pdo, $session, $msg, 3); foreach($vecResults as $vr) { $sim = round($vr["similarity"] * 100); $scope = ($vr["session_id"] === $session) ? "session" : "global"; $parts[] = "\xF0\x9F\x94\xAE [{$scope} {$sim}%] {$vr['content']}"; } } catch(Exception $e) {} return empty($parts) ? "" : implode("\n", array_unique(array_slice($parts, 0, 12))); } catch(Exception $e) { return ""; } } function updateMemory($pdo, $session, $msg, $resp, $intent) { try { if($intent === "greeting" || mb_strlen($msg) < 15) return; // Auto-embed new memories $autoEmbed = function($pdo, $id, $text) { try { $vec = generateEmbedding($text); if($vec && count($vec) == 768) { $vecStr = "[" . implode(",", $vec) . "]"; $pdo->prepare("UPDATE admin.wevia_memory SET embedding = ?::vector WHERE id = ?")->execute([$vecStr, $id]); } } catch(Exception $e) {} }; $l = mb_strtolower($msg); // ═══ ENTITY EXTRACTION — Names, companies, technologies ═══ $entities = []; // Companies & products if(preg_match_all('/\b(SAP|Vistex|Odoo|Oracle|Salesforce|AWS|Azure|GCP|Huawei|Scaleway|OVH|Docker|Kubernetes|PowerBI|Tableau|Snowflake|Databricks|Microsoft|Google|Meta|Apple|IBM|Deloitte|McKinsey|BCG|KPMG|PwC|Accenture|Capgemini)\b/i', $msg, $m)) { $entities = array_unique(array_map('ucfirst', array_map('strtolower', $m[0]))); } // Proper names (capitalized words not at sentence start) if(preg_match_all('/(?prepare("INSERT INTO admin.wevia_memory (session_id, memory_type, key, value, category, confidence, source) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING") ->execute([$session, "topic", $key, $value, $intent, 0.7, "auto"]); // Store entities foreach(array_unique(array_slice($entities, 0, 3)) as $ent) { $eKey = "entity_" . preg_replace("/[^a-z0-9]/", "_", strtolower($ent)); $pdo->prepare("INSERT INTO admin.wevia_memory (session_id, memory_type, key, value, category, confidence, source) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING") ->execute([$session, "entity", $eKey, $ent . " (mentionné dans: " . substr($msg, 0, 100) . ")", $intent, 0.85, "extraction"]); } // Store facts foreach(array_slice($facts, 0, 2) as $fact) { $fKey = $fact[0] . "_" . date("Ymd_His") . "_" . rand(10,99); $pdo->prepare("INSERT INTO admin.wevia_memory (session_id, memory_type, key, value, category, confidence, source) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING") ->execute([$session, $fact[0], $fKey, $fact[1], $intent, 0.9, "extraction"]); } // ═══ SESSION SUMMARY — Every 10 messages, create a summary ═══ $msgCount = $pdo->prepare("SELECT count(*) FROM admin.wevia_memory WHERE session_id=? AND memory_type='topic'"); $msgCount->execute([$session]); $cnt = (int)$msgCount->fetchColumn(); if($cnt > 0 && $cnt % 10 === 0) { // Build summary from recent topics $recentTopics = $pdo->prepare("SELECT value FROM admin.wevia_memory WHERE session_id=? AND memory_type='topic' ORDER BY created_at DESC LIMIT 10"); $recentTopics->execute([$session]); $topics = array_column($recentTopics->fetchAll(PDO::FETCH_ASSOC), "value"); $summaryText = "Session résumé (" . count($topics) . " échanges): " . implode(" | ", array_map(fn($t) => substr($t, 0, 60), $topics)); $pdo->prepare("INSERT INTO admin.wevia_memory (session_id, memory_type, key, value, category, confidence, source) VALUES (?, 'summary', ?, ?, 'session', 0.95, 'auto_summary') ON CONFLICT DO NOTHING") ->execute([$session, "summary_" . date("Ymd_H"), substr($summaryText, 0, 500)]); } // ═══ UPDATE ACCESS STATS — Boost frequently accessed memories ═══ $pdo->prepare("UPDATE admin.wevia_memory SET access_count = access_count + 1, last_accessed = NOW() WHERE id IN (SELECT id FROM admin.wevia_memory WHERE session_id = ? AND memory_type IN ('entity','preference','identity') ORDER BY created_at DESC LIMIT 3)") ->execute([$session]); // ═══ VECTOR STORAGE — Embed message + response for semantic search ═══ if(mb_strlen($msg) > 30) { storeVector($pdo, $session, $msg, "user_query"); } if($resp && mb_strlen($resp) > 100) { // Store condensed response (first 500 chars) as vector storeVector($pdo, $session, substr($msg, 0, 100) . " | " . substr($resp, 0, 400), "qa_pair"); } } catch(Exception $e) {} } // ═══════════════════════════════════════════════════════════ // WEB SEARCH — SearXNG // ═══════════════════════════════════════════════════════════ function detectWebNeed($msg, $kbContext) { $l = mb_strtolower($msg); if(preg_match('/(recherche|cherche|google|actualit|news|aujourd|dernier|récent|2025|2026|prix|cours|bourse|météo|current|latest|today)/i', $l)) return true; if(mb_strlen($kbContext) < 50 && mb_strlen($msg) > 30 && !preg_match('/^(bonjour|salut|hello|hi|merci|ok|test)/i', $l)) return true; if(preg_match('/^(qu[ie]|comment|pourquoi|quand|où|combien|what|how|why|when|where)/i', $l) && mb_strlen($msg) > 40) return true; return false; } function searchWeb($query) { try { $url = "http://127.0.0.1:8888/search?" . http_build_query([ "q" => mb_substr($query, 0, 150), "format" => "json", "language" => "fr", "categories" => "general", "engines" => "google,duckduckgo,bing,brave", "safesearch" => 1, ]); $ctx = stream_context_create(["http" => ["timeout" => 5, "header" => "Accept: application/json\r\n"]]); $res = @file_get_contents($url, false, $ctx); if(!$res) return null; $data = json_decode($res, true); if(!$data || empty($data["results"])) return null; $context = ""; $sources = []; $seen = []; foreach(array_slice($data["results"], 0, 5) as $r) { $content = $r["content"] ?? ""; if(empty($content) || mb_strlen($content) < 30) continue; $key = mb_substr($content, 0, 50); if(isset($seen[$key])) continue; $seen[$key] = true; $context .= "### {$r['title']}\n{$content}\n\n"; if($r["url"] ?? "") $sources[] = $r["url"]; } return empty($context) ? null : ["context" => mb_substr($context, 0, 4000), "sources" => $sources]; } catch(Exception $e) { return null; } } // ═══════════════════════════════════════════════════════════ // DOUBLE-CHECK — Critic model verification // ═══════════════════════════════════════════════════════════ function doubleCheck($response, $originalQuestion) { global $PROVIDERS, $PROMPT_CRITIC, $_usedProviderName; // TRUE CROSS-CHECK: critic must be DIFFERENT type than generator // If generator was GPU → critic = Cloud (Groq/Cerebras) // If generator was Cloud → critic = GPU (qwen/8b) $genWasGPU = in_array($_usedProviderName ?? "", ["deepseek_gpu","gpu_coder","gpu_reason","gpu_general","gpu_fast"]); if($genWasGPU) { // Generator=GPU → Critic=Cloud (different architecture = real cross-check) $criticOrder = ["cerebras","groq","sambanova","cohere"]; } else { // Generator=Cloud → Critic=GPU (sovereign cross-check) $criticOrder = ["gpu_coder","deepseek_gpu","cerebras","sambanova"]; } $criticProv = null; foreach($criticOrder as $cp) { if(isset($PROVIDERS[$cp]) && !empty($PROVIDERS[$cp]["key"])) { $criticProv = $PROVIDERS[$cp]; break; } } if(!$criticProv) return "OK"; $checkMsg = "Question originale: {$originalQuestion}\n\nRéponse à vérifier:\n" . substr($response, 0, 2000); $result = callAPI($criticProv, $PROMPT_CRITIC, $checkMsg); if(!$result || strlen($result) < 5) { // Try next in order foreach(array_slice($criticOrder, 1) as $fb) { if(isset($PROVIDERS[$fb]) && !empty($PROVIDERS[$fb]["key"])) { $result = callAPI($PROVIDERS[$fb], $PROMPT_CRITIC, $checkMsg); if($result && strlen($result) > 5) break; } } } return $result ?: "OK"; } // ═══════════════════════════════════════════════════════════ // SENTINEL TOOL-USE — Local server command execution // ═══════════════════════════════════════════════════════════ function executeSentinel($command) { // Security: whitelist safe read-only commands $allowed = ['df', 'free', 'uptime', 'ps', 'docker', 'systemctl status', 'cat /etc/hostname', 'ls', 'wc', 'head', 'tail', 'grep', 'find', 'du', 'netstat', 'ss', 'pg_isready', 'curl -s http://localhost']; $safe = false; foreach($allowed as $a) { if(strpos($command, $a) === 0) { $safe = true; break; } } if(!$safe) return "⚠️ Commande non autorisée. Seules les commandes de diagnostic sont permises."; $output = []; exec("timeout 5 " . escapeshellcmd($command) . " 2>&1", $output, $rc); $result = implode("\n", array_slice($output, 0, 50)); return $result ?: "(Pas de sortie, code retour: {$rc})"; } // ═══════════════════════════════════════════════════════════ // CODE SANDBOX — Python // ═══════════════════════════════════════════════════════════ function executePythonSandbox($code) { $forbidden = ['os.system', 'subprocess', 'exec(', 'eval(', '__import__', 'shutil', 'rmdir', 'unlink', 'remove(']; foreach($forbidden as $f) { if(stripos($code, $f) !== false) return "⚠️ Opération non autorisée en sandbox."; } $tmp = tempnam("/tmp", "wevia_py_"); $code = ltrim($code, "\n\r\t "); // Auto-detect PDF generation: set output dir $outputDir = "/var/www/weval/wevia-ia/downloads"; if(!is_dir($outputDir)) @mkdir($outputDir, 0755, true); $code = str_replace('/tmp/output.pdf', $outputDir . '/output_' . uniqid() . '.pdf', $code); // If code generates a PDF but no explicit path, inject one if(stripos($code, 'FPDF') !== false && stripos($code, '.output(') !== false && stripos($code, '/downloads/') === false) { $pdfFile = 'output_' . uniqid() . '.pdf'; $code = preg_replace('/\.output\(["\'](.*?)["\']\)/', '.output("' . $outputDir . '/' . $pdfFile . '")', $code); } // Auto-fix FPDF: wrap multi_cell to prevent width crash if(stripos($code, "FPDF") !== false) { // Ensure set_auto_page_break is set if(stripos($code, "set_auto_page_break") === false) { $code = preg_replace("/(pdf\s*=\s*FPDF\(\))/i", "\npdf.set_auto_page_break(auto=True, margin=15)", $code); } // Replace multi_cell(0, with safe version that checks available width $code = str_replace("pdf.multi_cell(0,", "pdf.multi_cell(pdf.epw,", $code); // Auto-fix deprecated ln= parameter $code = preg_replace('/\.cell\(([^)]*),\s*0,\s*1,\s*"([CLR])"\)/', '.cell($1, new_x="LMARGIN", new_y="NEXT", align="$2")', $code); $code = preg_replace('/\.cell\(([^)]*),\s*1,\s*0\)/', '.cell($1, new_x="RIGHT", new_y="TOP")', $code); // Wrap entire code in try/except for FPDFException $code = $code . " " ; } file_put_contents($tmp, "#!/usr/bin/env python3 import sys,warnings warnings.filterwarnings('ignore') sys.stdout = open(sys.stdout.fileno(), 'w', encoding='utf-8', errors='replace') " . $code); exec("timeout 10 python3 -W ignore {$tmp} 2>/dev/null", $out, $rc); @unlink($tmp); $result = implode("\n", $out); // Check if a PDF was generated $pdfFiles = glob("/var/www/weval/wevia-ia/downloads/output_*.pdf"); if($pdfFiles) { $latest = end($pdfFiles); $fname = basename($latest); $link = "/wevia-ia/downloads/{$fname}"; $extra = "\n\n📄 **PDF généré !** [⬇ Télécharger le PDF]({$link})"; return ($rc !== 0 ? "⚠️ Erreur (code {$rc}):\n{$result}" : ($result ?: "PDF créé")) . $extra; } return $rc !== 0 ? "⚠️ Erreur (code {$rc}):\n{$result}" : ($result ?: "(OK, pas de sortie)"); } // ═══════════════════════════════════════════════════════════ // LEARNING LOG // ═══════════════════════════════════════════════════════════ function logLearning($pdo, $msg, $resp, $provider, $latency, $usedWeb, $usedKB, $intent, $verified) { try { $pdo->exec("CREATE TABLE IF NOT EXISTS admin.nexus_log (id SERIAL PRIMARY KEY, question TEXT, answer_length INT, provider VARCHAR(50), latency_ms INT, used_web BOOLEAN DEFAULT FALSE, used_kb BOOLEAN DEFAULT FALSE, intent VARCHAR(50), verified BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT NOW())"); $pdo->prepare("INSERT INTO admin.nexus_log (question, answer_length, provider, latency_ms, used_web, used_kb, intent, verified) VALUES (?,?,?,?,?,?,?,?)") ->execute([substr($msg, 0, 200), strlen($resp), $provider, $latency, $usedWeb, $usedKB, $intent, $verified]); } catch(Exception $e) {} } // ═══════════════════════════════════════════════════════════ // API CALL // ═══════════════════════════════════════════════════════════ // VISION API — Analyze images with Llama 4 Scout // ═══════════════════════════════════════════════════════════ function callVisionAPI($imageBase64, $mimeType, $prompt, $sys) { global $PROVIDERS; $userPrompt = $prompt ?: "Analyse cette image en detail. Decris ce que tu vois."; // === TIER 1: SOVEREIGN GPU — Moondream (local, 0 cost, 0 latency réseau) === try { $ch = curl_init("http://127.0.0.1:11434/api/chat"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_TIMEOUT => 20, CURLOPT_POSTFIELDS => json_encode([ "model" => "moondream:latest", "messages" => [["role"=>"user","content"=>$userPrompt,"images"=>[$imageBase64]]], "stream" => false ]) ]); $res = curl_exec($ch); $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if($http == 200) { $d = json_decode($res, true); $content = $d["message"]["content"] ?? ""; if(strlen($content) > 20) { error_log("WEVIA_VISION: SOVEREIGN moondream OK (" . strlen($content) . " chars)"); return $content; } } } catch(Exception $e) { error_log("WEVIA_VISION: moondream error: " . $e->getMessage()); } // === TIER 2: CLOUD FALLBACK — Groq Vision (Llama 4 Scout 17B) === $p = $PROVIDERS["groq_vision"] ?? null; if(!$p) return "Vision non disponible."; $messages = [ ["role"=>"system","content"=>$sys], ["role"=>"user","content"=>[ ["type"=>"text","text"=>$userPrompt], ["type"=>"image_url","image_url"=>["url"=>"data:{$mimeType};base64,{$imageBase64}"]] ]] ]; $ch = curl_init($p["url"]); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ["Content-Type: application/json", "Authorization: Bearer ".$p["key"]], CURLOPT_POSTFIELDS => json_encode(["model"=>$p["model"],"messages"=>$messages,"max_tokens"=>2048,"temperature"=>0.5]), CURLOPT_TIMEOUT => 25 ]); $res = curl_exec($ch); $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if($http >= 400) { error_log("WEVIA_VISION: groq_vision FAIL HTTP $http"); return null; } $d = json_decode($res, true); if(isset($d["choices"][0]["message"]["content"])) { error_log("WEVIA_VISION: FALLBACK groq_vision OK"); return $d["choices"][0]["message"]["content"]; } if(isset($d["message"]["content"][0]["text"])) return $d["message"]["content"][0]["text"]; return null; } // ═══════════════════════════════════════════════════════════ // MEMCELL AUTO-POPULATION — Extract entities from conversations // ═══════════════════════════════════════════════════════════ function autoPopulateMemcells($pdo, $session, $msg, $resp, $intent) { try { $entities = []; if(preg_match_all('/\\b([A-Z][a-zA-Z]{2,}(?:\\s[A-Z][a-zA-Z]+)*)\\b/', $msg.' '.$resp, $m)) { foreach(array_unique($m[1]) as $e) { if(strlen($e) > 3 && !in_array(strtolower($e), ['bonjour','merci','comment','pourquoi','the','and','for'])) $entities[] = $e; } } if(empty($entities)) return; foreach(array_slice($entities, 0, 3) as $entity) { $ck = $pdo->prepare("SELECT id FROM admin.memcells WHERE entity=? AND session_id=? LIMIT 1"); $ck->execute([$entity, $session]); if($ck->fetchColumn()) { $pdo->prepare("UPDATE admin.memcells SET access_count=access_count+1, last_accessed=NOW() WHERE entity=? AND session_id=?")->execute([$entity, $session]); } else { $pdo->prepare("INSERT INTO admin.memcells (entity,fact,category,confidence,session_id,access_count,last_accessed) VALUES (?,?,?,0.7,?,1,NOW())") ->execute([$entity, substr($msg,0,200), $intent, $session]); } } if(count($entities) >= 2) { $s1 = $pdo->prepare("SELECT id FROM admin.memcells WHERE entity=? AND session_id=? LIMIT 1"); $s1->execute([$entities[0], $session]); $id1 = $s1->fetchColumn(); $s2 = $pdo->prepare("SELECT id FROM admin.memcells WHERE entity=? AND session_id=? LIMIT 1"); $s2->execute([$entities[1], $session]); $id2 = $s2->fetchColumn(); if($id1 && $id2) { $pdo->prepare("INSERT INTO admin.memcell_relations (source_id,target_id,relation_type,weight) VALUES (?,?,?,0.5) ON CONFLICT DO NOTHING") ->execute([$id1, $id2, 'co_mentioned']); } } } catch(Exception $e) {} } // ═══════════════════════════════════════════════════════════ function callAPI($p, $sys, $msg, $history = [], $fastMode = false) { error_log("WEVIA_CALLAPI: provider=" . ($p["model"] ?? "?") . " sysLen=" . strlen($sys) . " msgLen=" . strlen($msg) . " fast=" . ($fastMode ? "Y" : "N")); // PERF_SYS_SIZE // CALLAPI_RESP_DEBUG marker $__debugProvider = $p["model"] ?? "?"; $ch = curl_init($p["url"]); $messages = [["role"=>"system","content"=>$sys]]; if($history) { foreach(array_slice($history, -10) as $h) { $messages[] = ["role"=>$h["role"], "content"=>substr($h["content"], 0, 4000)]; } } $messages[] = ["role"=>"user","content"=>$msg]; // PDF one-shot injection - teach LLMs to use [EXEC:python]...[/EXEC] $isPdfReq = preg_match("/\b(pdf|PDF|document|rapport|report|genere.*fichier|cree.*fichier)\b/i", $msg); if($isPdfReq) { // Insert one-shot example BEFORE the user message array_splice($messages, -1, 0, [ ["role" => "user", "content" => "genere un pdf"], ["role" => "assistant", "content" => "Voici ton PDF :\n\n[EXEC:python]\nfrom fpdf import FPDF\npdf = FPDF()\npdf.add_font('DejaVu', '', '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf')\npdf.add_font('DejaVu', 'B', '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf')\npdf.add_page()\npdf.set_font('DejaVu', 'B', 18)\npdf.cell(0, 12, text='Mon Document', ln=True, align='C')\npdf.ln(5)\npdf.set_font('DejaVu', '', 11)\npdf.multi_cell(0, 7, text='Contenu professionnel ici.')\npdf.output('/var/www/weval/wevia-ia/downloads/document.pdf')\n[/EXEC]"] ]); } // Detect Ollama early (needed for both native and compat paths) $isOllama = (strpos($p["url"], "11434") !== false || ($p["key"] ?? "") === "ollama"); // Ollama native API format (/api/chat) if ($isOllama && $p["type"] === "ollama") { $ollamaPayload = json_encode([ "model" => $p["model"], "messages" => $messages, "stream" => false, "options" => ["num_ctx" => 16384, "num_predict" => 2048] ]); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_POSTFIELDS => $ollamaPayload, CURLOPT_TIMEOUT => 20 // Via SSH tunnel to S88 GPU ]); $r = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code === 200 && $r) { $d = json_decode($r, true); $content = $d["message"]["content"] ?? ""; // Strip blocks $content = preg_replace("/.*?<\/think>/s", "", $content); $content = str_replace(["", ""], "", $content); return trim($content) ?: null; } return null; } // Optimize for local Ollama models: trim system prompt, skip auth // $isOllama already defined above if ($isOllama) { // Smart trim: keep core rules + KB context, scale by model $model = $p["model"] ?? ""; $maxSys = 6000; // default for 8b models if(strpos($model,"32b")!==false || strpos($model,"r1")!==false) $maxSys = 12000; if(strpos($model,"14b")!==false || strpos($model,"coder")!==false) $maxSys = 10000; if(strlen($messages[0]["content"]) > $maxSys) { // Keep first half (core rules) + last half (KB context near the question) $half = intval($maxSys / 2); $messages[0]["content"] = mb_substr($messages[0]["content"], 0, $half) . "\n\n" . mb_substr($messages[0]["content"], -$half); } } // ═══ SYSPROMPT_CAP — Trim for cloud providers (8K context limits) ═══ $maxSysCloud = 6000; // Safe for Groq/Cerebras/Sambanova free tier if (!$isOllama && strlen($messages[0]["content"]) > $maxSysCloud) { // Keep WEVAL identity (first 2000 chars) + last context (KB/cognitive near the question) $head = mb_substr($messages[0]["content"], 0, 2500); $tail = mb_substr($messages[0]["content"], -3000); $messages[0]["content"] = $head . "\n\n[...]\n\n" . $tail; error_log("WEVIA_SYSPROMPT_CAP: trimmed from " . strlen($sys) . " to " . strlen($messages[0]["content"]) . " for " . ($p["model"] ?? "?")); } $headers = ["Content-Type: application/json"]; if (!$isOllama) $headers[] = "Authorization: Bearer ".$p["key"]; $payload = [ "model" => $p["model"], "messages" => $messages, "max_tokens" => $fastMode ? 300 : (($isPdfReq ?? false) ? 4096 : ($GLOBALS["_cogMaxTokens"] ?? (($GLOBALS["_mode"]??"deep")==="fast" ? 800 : 3000))), "temperature" => $GLOBALS["_cogTemp"] ?? (($GLOBALS["_mode"]??"deep")==="fast" ? 0.3 : 0.6), "stream" => false ]; // Trim system prompt for Ollama (limited context window) if(strpos($p["url"]??"","11434") !== false && strlen($sys) > 8000) { // CRITICAL: Inject visual instructions at TOP so they survive trimming $visualRules = "RÈGLES VISUELLES OBLIGATOIRES:\n" . "1) Photo/image/logo demandé → réponds [IMAGE:description en anglais détaillée]. EXCEPTION: si l'utilisateur demande un graphique des CAUSES, un diagramme cause-effet, ou un Ishikawa → utilise ```mermaid avec graph LR, PAS [IMAGE].\n" . "2) Schema/diagramme/ishikawa/processus/flowchart demandé → TOUJOURS utiliser ```mermaid\nflowchart TD\n...``` avec max 10 noeuds, JAMAIS de style/classDef.\n" . "3) PDF/rapport demandé → utilise le code Python FPDF.\n" . "4) INTERDICTION de décrire une image en texte. Tu GÉNÈRES, tu ne décris pas.\n\n"; // Keep first 3500 (core) + last 3500 (KB) + visual rules at top $sys = $visualRules . mb_substr($sys, 0, 3500) . "\n\n" . mb_substr($sys, -3500); } // Scale context window by model size (8b=8K, 14b+=16K) if ($isOllama) { $model = $p["model"] ?? ""; $ctxSize = 8192; // default for 8b if(strpos($model, "32b") !== false || strpos($model, "r1") !== false) $ctxSize = 16384; if(strpos($model, "14b") !== false || strpos($model, "coder") !== false) $ctxSize = 16384; $payload["options"] = ["num_ctx" => $ctxSize, "num_predict" => 2048]; } curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_TIMEOUT => $fastMode ? 8 : ($isPdfReq ? 60 : (strpos($p["url"]??"","11434")!==false ? 8 : (($GLOBALS["_mode"]??"deep")==="fast" ? 15 : 30))) ]); $res = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlErr = curl_error($ch); curl_close($ch); error_log("CURL_RAW_DEBUG: prov=" . ($p["model"] ?? "?") . " http=$httpCode err=$curlErr rawLen=" . strlen($res ?? "") . " raw100=" . substr($res ?? "", 0, 150)); if($httpCode >= 400) { $errType = ($httpCode == 429) ? "RATE_LIMIT" : "HTTP_ERR"; error_log("WEVIA_{$errType}: model={$p['model']} http=$httpCode"); file_put_contents("/tmp/wevia_debug.log", date("H:i:s")." {$errType} model={$p['model']} http=$httpCode body=".substr($res??"",0,200)."\n", FILE_APPEND); return null; } $d = json_decode($res, true); // OpenAI format if(isset($d["choices"][0]["message"]["content"])) return $d["choices"][0]["message"]["content"]; // Ollama native format (message.content = string) if(isset($d["message"]["content"]) && is_string($d["message"]["content"])) return $d["message"]["content"]; // Cohere v2 format (message.content = array of objects) if(isset($d["message"]["content"][0]["text"])) return $d["message"]["content"][0]["text"]; return null; } // ═══ PUBLIC RESPONSE SANITIZER ═══ function sanitizePublicResponse($text) { if(!$text || strlen(trim($text)) < 2) return $text; // Strip neuro-symbolique engine markers $text = preg_replace('/\n---\n.*Moteur neuro.*/s', '', $text) ?? $text; // Strip Chain-of-Thought leaks (LLM internal reasoning) $cotPatterns = [ '/^(We need to|I need to|Let me|I should|The spec mentions|Possibly we|But to be safe|To be safe)[^.\n]{10,}[.\n]/mi', '/^(Thinking|Planning|Reasoning|Step \d|Option \d)[:\s][^\n]{10,}\n/mi', '/^(Note:|Observation:|Reflection:|Assessment:)[^\n]+\n/mi', '/.*?<\/think>/si', '/.*?<\/reasoning>/si', ]; foreach($cotPatterns as $p) { $text = preg_replace($p, '', $text) ?? $text; } // Strip markdown $text = preg_replace('/\*\*([^*]+)\*\*/', '$1', $text) ?? $text; $text = preg_replace('/\*([^*]+)\*/', '$1', $text) ?? $text; // Convert lists to prose (safe regexes) $text = preg_replace('/^\d+[.)\s]+/m', '', $text) ?? $text; $text = preg_replace('/^[-*]\s+/m', '', $text) ?? $text; // Strip IP addresses $text = preg_replace('/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/', '[confidentiel]', $text) ?? $text; // Strip SSH/port info $text = preg_replace('/port\s*(?:SSH)?\s*:?\s*\d{2,5}/i', 'port [confidentiel]', $text) ?? $text; $text = preg_replace('/utilisateur\s+SSH\s*:?\s*\w+/i', 'utilisateur [confidentiel]', $text) ?? $text; $text = preg_replace('/\b(password|mot de passe|mdp)\s*:?\s*\S+/i', '[confidentiel]', $text) ?? $text; // Strip system files $text = preg_replace('/\w+:x:\d+:\d+:[^:]*:[^:]*:[^\n]*/m', '[masque]', $text) ?? $text; // Strip sensitive terms $sensitive = ['WEVADS','PowerMTA','pmta','adx_system','adx_clients','brain_send_configs', 'brain_seeds','warmup_accounts','sentinel-brain','OVH Tracking', 'culturellemejean','wevup.app','mta-eu','bcg_local','Brain Engine', 'brain_configs','brain_offer','warmup_queue','send_queue']; foreach ($sensitive as $s) { $text = str_ireplace($s, '[confidentiel]', $text); } // Strip SSH if(empty($GLOBALS['_sshConnected'])) { $text = preg_replace('/ssh\s+\S+@\S+/i', '[confidentiel]', $text) ?? $text; } $text = preg_replace('/root@\S+/', '[confidentiel]', $text) ?? $text; // Strip server IPs $text = preg_replace('/\b(89\.167\.\d+\.\d+|151\.80\.\d+\.\d+|157\.180\.\d+\.\d+|88\.198\.\d+\.\d+|46\.62\.\d+\.\d+)\b/', '[IP masquée]', $text) ?? $text; $text = str_ireplace(['e36lbat.com','rivoweb','Arsenal'], ['[domaine]','[domaine]','[confidentiel]'], $text); // Strip DB details $text = preg_replace('/pgsql:host=\S+/i', '[confidentiel]', $text) ?? $text; $text = preg_replace('/dbname=\w+/i', '[confidentiel]', $text) ?? $text; $text = preg_replace('/:\b(5821|5880|5890|49222|5678)\b/', ':[confidentiel]', $text) ?? $text; // Strip email marketing refs // Only mask truly sensitive infrastructure terms, NOT business topics $emailInfraTerms = []; // email marketing terms are allowed for educational content foreach ($emailInfraTerms as $et) { $text = str_ireplace($et, 'optimisation digitale', $text); } // Strip server paths $text = preg_replace('#/var/www/weval/wevia-ia/downloads/#', '/wevia-ia/downloads/', $text) ?? $text; $text = preg_replace('#/var/www/weval/[\w\-./]+#', '[chemin serveur]', $text) ?? $text; if(strpos($text, 'Erreur PDF') === false) { $text = preg_replace('#/tmp/[\w\-./]+#', '[fichier temporaire]', $text) ?? $text; } $text = preg_replace('#/opt/[\w\-./]+#', '[chemin interne]', $text) ?? $text; // Strip SENTINEL tags $text = preg_replace('/\[SENTINEL:[^\]]*\]/i', '', $text) ?? $text; // Clean multiple blank lines $text = preg_replace('/\n{3,}/', "\n\n", $text) ?? $text; $text = trim($text); // Strip raw HTML that shouldn't appear in chat $text = preg_replace('/]*class=["\'](rule|dark|kb|brain)[^>]*>.*?<\/div>/si', '', $text) ?? $text; $text = preg_replace('/<\/div>/', '', $text) ?? $text; // Strip XSS $text = str_ireplace('', '</script>', $text); // ═══ EMOJI POST-PROCESSING ═══ // Replace banned confused emojis with positive ones $banned = ['🤔','🤷','😕','😐','🙄','😬']; foreach($banned as $b) { $text = str_replace($b, '✨', $text); } // If response has zero emojis and is long enough, inject one $hasEmoji = preg_match('/[\x{1F300}-\x{1F9FF}\x{2600}-\x{27BF}\x{FE00}-\x{FE0F}\x{1F000}-\x{1F02F}\x{1F0A0}-\x{1F0FF}\x{1F100}-\x{1F64F}\x{1F680}-\x{1F6FF}\x{1F900}-\x{1F9FF}]/u', $text); if(!$hasEmoji && mb_strlen($text) > 40) { $posEmojis = ['✨','💡','🚀','😊','🎯','🔥','💪','🌟']; $emoji = $posEmojis[array_rand($posEmojis)]; // PROTECT markdown images/links from emoji injection $protectedParts = []; $text = preg_replace_callback('/(!?\\[[^\\]]*\\]\\([^)]+\\))/', function($m) use (&$protectedParts) { $key = 'XPROT' . count($protectedParts) . 'X'; $protectedParts[$key] = $m[0]; return $key; }, $text); // Add after first sentence-ending period (skip .png .pdf etc) if(preg_match('/(? $val) { $text = str_replace($key, $val, $text); } } // Rebrand $text = str_ireplace('HAMID', 'WEVAL Brain', $text); return $text; }