'message required']); exit; } function load_secrets() { $s = []; if (!is_readable('/etc/weval/secrets.env')) return $s; foreach (file('/etc/weval/secrets.env', FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $l) { if (empty(trim($l))||$l[0]==='#') continue; $p = strpos($l,'='); if ($p) $s[trim(substr($l,0,$p))] = trim(substr($l,$p+1)," \t\"'"); } return $s; } function pg_c() { return @pg_connect('host=10.1.0.3 port=5432 dbname=paperclip user=admin password=admin123 connect_timeout=2'); } // ═══════════════════════════════════════════════════════════════════ // MEMORY BACKEND · Redis DB 5 (unlimited for internal, 1h TTL for public) // ═══════════════════════════════════════════════════════════════════ function mem_open() { try { $r = new Redis(); $r->connect('127.0.0.1', 6379, 1.5); $r->select(5); // DB 5 dedicated to chat memory return $r; } catch (Exception $e) { return null; } } function mem_key($chat_id, $user_id) { return "chatmem:{$chat_id}:{$user_id}"; } function mem_load($chat_id, $user_id, $limit = 30) { $r = mem_open(); if (!$r) return []; $raw = $r->get(mem_key($chat_id, $user_id)); $history = $raw ? (json_decode($raw, true) ?: []) : []; return array_slice($history, -$limit); } function mem_save($chat_id, $user_id, $history, $scope) { $r = mem_open(); if (!$r) return false; // Internal chats: NO TTL (unlimited persistent) // Public chats: 1h TTL (privacy-respecting session-only) if ($scope === 'internal') { // Keep last 500 messages (reasonable cap, no TTL) $history = array_slice($history, -500); $r->set(mem_key($chat_id, $user_id), json_encode($history)); } else { $history = array_slice($history, -20); $r->setex(mem_key($chat_id, $user_id), 3600, json_encode($history)); } return true; } function mem_stats($chat_id, $user_id) { $r = mem_open(); if (!$r) return ['keys'=>0, 'messages'=>0]; $key = mem_key($chat_id, $user_id); $exists = $r->exists($key); $raw = $exists ? $r->get($key) : ''; $history = $raw ? (json_decode($raw, true) ?: []) : []; return [ 'keys' => $exists ? 1 : 0, 'messages' => count($history), 'ttl' => $r->ttl($key), 'persistent' => ($r->ttl($key) === -1), ]; } // ═══════════════════════════════════════════════════════════════════ // GROUNDING · reuse same PostgreSQL logic as wave 253 saas-chat // ═══════════════════════════════════════════════════════════════════ function build_grounding() { $ctx = ['ts' => date('c')]; $pg = pg_c(); if ($pg) { $r = @pg_query($pg, "SELECT COUNT(*) AS n, ROUND(AVG(mql_score)) AS avg_mql FROM weval_leads"); if ($r) $ctx['leads'] = pg_fetch_assoc($r); $r2 = @pg_query($pg, "SELECT industry, COUNT(*) AS n FROM weval_leads WHERE industry IS NOT NULL GROUP BY industry ORDER BY n DESC LIMIT 5"); if ($r2) { $ctx['industries'] = []; while ($row = pg_fetch_assoc($r2)) $ctx['industries'][] = $row; } $r3 = @pg_query($pg, "SELECT company, mql_score, industry FROM weval_leads WHERE mql_score >= 85 ORDER BY mql_score DESC LIMIT 5"); if ($r3) { $ctx['top_leads'] = []; while ($row = pg_fetch_assoc($r3)) $ctx['top_leads'][] = $row; } pg_close($pg); } return $ctx; } // Sanitizer wave 256 function wave258_sanitize($t) { if (!is_string($t) || $t === '') return $t; $bl = ['Groq','Cerebras','SambaNova','Ollama','DeepSeek','Mistral','Together','Replicate', 'vLLM','Qwen','NVIDIA NIM','Cohere','OpenRouter','HuggingFace','Anthropic', '/opt/','/var/www/','/etc/','admin123','49222','11434','6333','4001', '204.168','95.216','151.80','10.1.0','root@','ssh -p','docker ps', 'PGPASSWORD','PostgreSQL','weval_leads','weval_tasks']; foreach ($bl as $w) $t = str_ireplace($w, 'WEVIA Engine', $t); $t = preg_replace('/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/', '[infrastructure securisee]', $t); $t = preg_replace('/\b(sk-[a-zA-Z0-9]{20,}|xoxb-[a-zA-Z0-9-]{20,}|eyJ[a-zA-Z0-9_.-]{50,})\b/', '[token securise]', $t); return $t; } // ═══════════════════════════════════════════════════════════════════ // LOAD HISTORY + GROUNDING // ═══════════════════════════════════════════════════════════════════ $history = mem_load($chat_id, $user_id, 30); $grounding = build_grounding(); // System prompt with unlimited memory context + grounding $ground_text = "DONNÉES LIVE WEVAL:\n"; if (!empty($grounding['leads'])) $ground_text .= "· Leads: " . $grounding['leads']['n'] . " (avg MQL " . $grounding['leads']['avg_mql'] . ")\n"; if (!empty($grounding['industries'])) { $ground_text .= "· Industries: "; foreach ($grounding['industries'] as $i) $ground_text .= $i['industry']."(".$i['n'].") "; $ground_text .= "\n"; } if (!empty($grounding['top_leads'])) { $ground_text .= "· TOP: "; foreach ($grounding['top_leads'] as $tl) $ground_text .= $tl['company']."(MQL".$tl['mql_score'].") "; $ground_text .= "\n"; } $mem_info = ""; if ($scope === 'internal' && count($history) > 0) { $mem_info = "\n🧠 MÉMOIRE PERSISTENTE (" . count($history) . " messages en historique, unlimited, chat_id=$chat_id):\n"; $mem_info .= "Tu as accès à l'historique complet de cette conversation interne. Utilise-le pour contextualiser.\n"; } $system_prompt = "Tu es WEVIA Master (chat interne $chat_id) de WEVAL Consulting.\n\n" . "Mode: " . strtoupper($scope) . " · " . ($scope === 'internal' ? 'mémoire illimitée persistente' : 'mémoire session only (privacy)') . "\n\n" . "RÈGLES:\n" . "1. Utilise UNIQUEMENT les données live ci-dessous\n" . "2. JAMAIS dire 'je n'ai pas accès' — utilise les données fournies\n" . "3. Français naturel, concis, actionnable\n\n" . $ground_text . $mem_info; // Build messages (system + history + user) $messages = [['role'=>'system', 'content'=>$system_prompt]]; foreach ($history as $h) { $messages[] = ['role' => $h['role'], 'content' => $h['content']]; } $messages[] = ['role'=>'user', 'content'=>$message]; // Cascade LLM $secrets = load_secrets(); $providers = [ ['name'=>'Groq', 'url'=>'https://api.groq.com/openai/v1/chat/completions', 'key'=>$secrets['GROQ_KEY']??'', 'model'=>'llama-3.3-70b-versatile'], ['name'=>'Cerebras', 'url'=>'https://api.cerebras.ai/v1/chat/completions', 'key'=>$secrets['CEREBRAS_API_KEY']??'', 'model'=>'llama-3.3-70b'], ['name'=>'Mistral', 'url'=>'https://api.mistral.ai/v1/chat/completions', 'key'=>$secrets['MISTRAL_KEY']??'', 'model'=>'mistral-small-latest'], ]; $reply = ''; $provider_used = ''; foreach ($providers as $p) { if (empty($p['key'])) continue; $ch = curl_init($p['url']); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_TIMEOUT=>15, CURLOPT_HTTPHEADER=>['Content-Type: application/json', 'Authorization: Bearer '.$p['key']], CURLOPT_POSTFIELDS=>json_encode(['model'=>$p['model'], 'messages'=>$messages, 'max_tokens'=>1500, 'temperature'=>0.3]) ]); $r = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code >= 200 && $code < 300) { $d = json_decode($r, true); $reply = $d['choices'][0]['message']['content'] ?? ''; if ($reply) { $provider_used = $p['name']; break; } } } if (!$reply) $reply = 'Service temporairement indisponible.'; // Sanitize response $reply = wave258_sanitize($reply); // Save to memory (internal=unlimited, public=session) $history[] = ['role'=>'user', 'content'=>$message]; $history[] = ['role'=>'assistant', 'content'=>$reply]; mem_save($chat_id, $user_id, $history, $scope); $stats = mem_stats($chat_id, $user_id); echo json_encode([ 'response' => $reply, 'provider' => 'WEVIA Engine', 'chat_id' => $chat_id, 'scope' => $scope, 'user_id' => substr($user_id, 0, 20) . '...', 'memory_stats' => $stats, 'wave' => 258, 'duration_ms' => round((microtime(true) - $t0) * 1000), ], JSON_UNESCAPED_UNICODE);