217 lines
10 KiB
PHP
217 lines
10 KiB
PHP
<?php
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
WEVIA CHAT MEMORY · Wave 258
|
|
|
|
Unified chat endpoint with UNLIMITED PERSISTENT MEMORY for INTERNAL chats.
|
|
Public widget (/ + /wevia) = session-only. Internal chats = unlimited.
|
|
|
|
- Stores full history in Redis DB 5 (no TTL for internal)
|
|
- Session identified by user_id (from cookie/JWT)
|
|
- Recent N messages loaded per query
|
|
- Qdrant vector memory for cross-session recall (long-term memory)
|
|
- Grounding via multi-agent factory
|
|
|
|
POST /api/wevia-chat-memory.php
|
|
{"message":"...", "chat_id":"wevia-master|all-ia-hub|command-center", "scope":"internal|public"}
|
|
|
|
Returns: {response, memory_stats, agents_used, grade}
|
|
═══════════════════════════════════════════════════════════════════ */
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Access-Control-Allow-Origin: *');
|
|
set_time_limit(45);
|
|
|
|
$t0 = microtime(true);
|
|
$input = json_decode(file_get_contents('php://input'), true) ?: [];
|
|
$message = trim($input['message'] ?? '');
|
|
$chat_id = $input['chat_id'] ?? 'default';
|
|
$scope = $input['scope'] ?? 'internal'; // internal = unlimited, public = session-only
|
|
$user_id = $input['user_id'] ?? ($_COOKIE['weval_chat_session'] ?? 'anon-' . substr(md5($_SERVER['REMOTE_ADDR'] ?? '') . ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 12));
|
|
|
|
if (!$message) { http_response_code(400); echo json_encode(['error'=>'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);
|