191 lines
10 KiB
PHP
191 lines
10 KiB
PHP
<?php
|
|
// WAVE 253 · saas-chat grounded with live WEVAL data (anti-hallucination)
|
|
header('Content-Type: application/json');
|
|
// WAVE 256: Defense-in-depth sanitizer (mirrors weval-ia-fast.php)
|
|
function wave256_sanitize($t) {
|
|
if (!is_string($t) || $t === '') return $t;
|
|
$blocklist = ['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 ($blocklist 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);
|
|
$t = preg_replace('/\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/', '[token securise]', $t);
|
|
return $t;
|
|
}
|
|
|
|
|
|
header('Access-Control-Allow-Origin: *');
|
|
if($_SERVER['REQUEST_METHOD']==='OPTIONS'){header('Access-Control-Allow-Methods: POST');header('Access-Control-Allow-Headers: Content-Type');exit;}
|
|
$input=json_decode(file_get_contents('php://input'),true);
|
|
|
|
// === DOCTRINE-141-SHUTDOWN · memory bridge ===
|
|
if (@file_exists(__DIR__.'/wevia-memory-bridge.php')) { @require_once __DIR__.'/wevia-memory-bridge.php'; }
|
|
$__chat_id = 'saas-chat';
|
|
$__user_id = $_COOKIE['weval_chat_session'] ?? $_SERVER['HTTP_X_USER_ID'] ?? ('anon-'.substr(md5(($_SERVER['REMOTE_ADDR']??'').($_SERVER['HTTP_USER_AGENT']??'')),0,12));
|
|
$__msg_for_mem = '';
|
|
register_shutdown_function(function() use (&$__msg_for_mem, $__chat_id, $__user_id) {
|
|
if (!function_exists('wevia_mem_save')) return;
|
|
$out = ob_get_contents();
|
|
if (!$out) return;
|
|
$d = @json_decode($out, true);
|
|
$resp = is_array($d) ? ($d['response'] ?? $d['answer'] ?? $d['content'] ?? '') : $out;
|
|
if ($resp && $__msg_for_mem) {
|
|
@wevia_mem_save($__chat_id, $__user_id, $__msg_for_mem, is_string($resp)?$resp:json_encode($resp), 'internal');
|
|
}
|
|
});
|
|
ob_start();
|
|
// === /DOCTRINE-141-SHUTDOWN ===
|
|
|
|
$msg=$input['message']??$_POST['message']??''; $__msg_for_mem = $msg;
|
|
$session=$input['session']??$_POST['session']??'default';
|
|
$origin=$input['origin']??$_SERVER['HTTP_REFERER']??'unknown';
|
|
if(!$msg)die(json_encode(['error'=>'no message']));
|
|
|
|
// === WAVE 253 GROUNDING: fetch live data BEFORE LLM call (anti-hallucination) ===
|
|
function pg_c() { return @pg_connect('host=10.1.0.3 port=5432 dbname=paperclip user=admin password=admin123 connect_timeout=2'); }
|
|
|
|
function build_grounding_context() {
|
|
$ctx = ['ts' => date('c'), 'source' => 'live'];
|
|
$pg = pg_c();
|
|
if ($pg) {
|
|
// 48 leads stats
|
|
$r = @pg_query($pg, "SELECT COUNT(*) AS n, ROUND(AVG(mql_score)) AS avg_mql, SUM(CASE WHEN sql_qualified THEN 1 ELSE 0 END) AS sql_q FROM weval_leads");
|
|
if ($r) { $ctx['leads'] = pg_fetch_assoc($r); }
|
|
// Top industries
|
|
$r2 = @pg_query($pg, "SELECT industry, COUNT(*) AS n, ROUND(AVG(mql_score)) AS avg_mql FROM weval_leads WHERE industry IS NOT NULL GROUP BY industry ORDER BY n DESC, avg_mql DESC LIMIT 8");
|
|
if ($r2) { $ctx['industries'] = []; while ($row = pg_fetch_assoc($r2)) $ctx['industries'][] = $row; }
|
|
// Top countries
|
|
$r3 = @pg_query($pg, "SELECT country, COUNT(*) AS n FROM weval_leads WHERE country IS NOT NULL GROUP BY country ORDER BY n DESC LIMIT 6");
|
|
if ($r3) { $ctx['countries'] = []; while ($row = pg_fetch_assoc($r3)) $ctx['countries'][] = $row; }
|
|
// TOP 5 leads with MQL 85+
|
|
$r4 = @pg_query($pg, "SELECT company, mql_score, industry, country, sql_qualified FROM weval_leads WHERE mql_score >= 85 ORDER BY mql_score DESC LIMIT 6");
|
|
if ($r4) { $ctx['top_leads'] = []; while ($row = pg_fetch_assoc($r4)) $ctx['top_leads'][] = $row; }
|
|
// Tasks
|
|
$r5 = @pg_query($pg, "SELECT status, COUNT(*) AS n, SUM(estimated_mad) AS mad FROM weval_tasks GROUP BY status");
|
|
if ($r5) { $ctx['tasks_by_status'] = []; while ($row = pg_fetch_assoc($r5)) $ctx['tasks_by_status'][] = $row; }
|
|
pg_close($pg);
|
|
}
|
|
// Solutions scanner top 3
|
|
$ch = curl_init('http://127.0.0.1/api/solution-scanner.php?action=full_analysis');
|
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>1, CURLOPT_TIMEOUT=>5]);
|
|
$raw = curl_exec($ch); curl_close($ch);
|
|
if ($raw) {
|
|
$sd = @json_decode($raw, true);
|
|
if ($sd && !empty($sd['solutions'])) {
|
|
$ctx['top_solutions'] = [];
|
|
foreach (array_slice($sd['solutions'], 0, 5) as $sol) {
|
|
$ctx['top_solutions'][] = [
|
|
'name' => $sol['name'],
|
|
'winning_score' => $sol['winning_score'],
|
|
'decision' => $sol['decision'],
|
|
'mad' => $sol['mad_est'],
|
|
'maturity' => $sol['maturity'],
|
|
'dev_calendar_days' => $sol['dev_effort']['calendar_days_est'] ?? 0,
|
|
];
|
|
}
|
|
$ctx['pipeline_summary'] = $sd['summary'] ?? null;
|
|
}
|
|
}
|
|
return $ctx;
|
|
}
|
|
|
|
$grounding = build_grounding_context();
|
|
|
|
// Build grounding text block for system prompt
|
|
$ground_text = "DONNÉES WEVAL LIVE (ne jamais dire que tu n'as pas accès - ces données sont fraîches, injectées):\n\n";
|
|
|
|
if (!empty($grounding['leads'])) {
|
|
$l = $grounding['leads'];
|
|
$ground_text .= "📊 PAPERCLIP LEADS: " . $l['n'] . " leads · avg MQL " . $l['avg_mql'] . " · SQL qualifiés " . $l['sql_q'] . "\n";
|
|
}
|
|
if (!empty($grounding['industries'])) {
|
|
$ground_text .= "🏭 TOP INDUSTRIES: ";
|
|
foreach ($grounding['industries'] as $i) $ground_text .= $i['industry'] . " (" . $i['n'] . " leads, MQL " . $i['avg_mql'] . ") · ";
|
|
$ground_text .= "\n";
|
|
}
|
|
if (!empty($grounding['countries'])) {
|
|
$ground_text .= "🗺 PAYS: ";
|
|
foreach ($grounding['countries'] as $c) $ground_text .= $c['country'] . "=" . $c['n'] . " · ";
|
|
$ground_text .= "\n";
|
|
}
|
|
if (!empty($grounding['top_leads'])) {
|
|
$ground_text .= "🏆 TOP LEADS MQL85+:\n";
|
|
foreach ($grounding['top_leads'] as $tl) {
|
|
$sql = ($tl['sql_qualified'] === 't' || $tl['sql_qualified'] === true) ? '✅SQL' : '';
|
|
$ground_text .= " · " . $tl['company'] . " (MQL " . $tl['mql_score'] . " · " . $tl['industry'] . " · " . $tl['country'] . " $sql)\n";
|
|
}
|
|
}
|
|
if (!empty($grounding['tasks_by_status'])) {
|
|
$ground_text .= "📋 TASKS: ";
|
|
foreach ($grounding['tasks_by_status'] as $t) $ground_text .= $t['status'] . "=" . $t['n'] . " (" . round(($t['mad']??0)/1000) . "K MAD) · ";
|
|
$ground_text .= "\n";
|
|
}
|
|
if (!empty($grounding['top_solutions'])) {
|
|
$ground_text .= "\n🎯 TOP 5 SOLUTIONS WEVAL (Scanner WePredict):\n";
|
|
foreach ($grounding['top_solutions'] as $s) {
|
|
$ground_text .= " · " . $s['name'] . " · score " . $s['winning_score'] . "/100 · " . $s['decision'] . " · " . round($s['mad']/1000) . "K MAD · maturity " . $s['maturity'] . "% · dev " . $s['dev_calendar_days'] . "j\n";
|
|
}
|
|
}
|
|
if (!empty($grounding['pipeline_summary'])) {
|
|
$ps = $grounding['pipeline_summary'];
|
|
$ground_text .= "\n💰 PIPELINE GLOBAL: " . round($ps['total_mad_pipeline']/1000) . "K MAD · dev cost " . round($ps['total_dev_cost_mad']/1000) . "K MAD · " . $ps['ship_it'] . " SHIP_IT · " . $ps['dev_sprint'] . " DEV_SPRINT\n";
|
|
}
|
|
|
|
// Redis memory
|
|
$history=[];
|
|
try{$redis=new Redis();$redis->connect('127.0.0.1',6379);$redis->select(3);
|
|
$raw=$redis->get("saas:$session");if($raw)$history=json_decode($raw,true)?:[];}catch(Exception $e){$redis=null;}
|
|
|
|
// System prompt ENRICHED with grounding
|
|
$system_prompt = "Tu es WEVIA Master, IA de WEVAL Consulting Casablanca (Maroc + France + MENA).\n\n" .
|
|
"RÈGLES STRICTES ANTI-HALLUCINATION (wave 253):\n" .
|
|
"1. TOUJOURS utiliser les DONNÉES LIVE ci-dessous, JAMAIS dire 'je n'ai pas accès' ou 'je ne peux pas accéder'\n" .
|
|
"2. Si on te demande 'combien de leads' → réponds avec le chiffre exact fourni\n" .
|
|
"3. Si on te demande top industries/pays/leads → liste ceux fournis\n" .
|
|
"4. Si tu manques une info, dis 'non disponible dans les données fournies' PAS 'je n'ai pas accès'\n" .
|
|
"5. Réponds en français concis. Utilise les noms réels (Ethica, Vistex, Huawei, etc.)\n\n" .
|
|
$ground_text . "\n" .
|
|
"CONTEXTE: origin=" . basename($origin) . "\n" .
|
|
"Réponds maintenant à la question en t'appuyant sur ces données live.";
|
|
|
|
$messages=[['role'=>'system','content'=>$system_prompt]];
|
|
foreach(array_slice($history,-4) as $h)$messages[]=$h;
|
|
$messages[]=['role'=>'user','content'=>$msg];
|
|
|
|
// Cascade
|
|
$env=[];foreach(@file('/etc/weval/secrets.env',2|4)?:[] as $l){if(strpos($l,'=')!==false){[$k,$v]=explode('=',$l,2);$env[trim($k)]=trim($v," \t\"'");}}
|
|
$providers=[
|
|
['u'=>'https://api.groq.com/openai/v1/chat/completions','k'=>$env['GROQ_KEY']??'','m'=>'llama-3.3-70b-versatile','name'=>'Groq-Llama3.3'],
|
|
['u'=>'https://api.cerebras.ai/v1/chat/completions','k'=>$env['CEREBRAS_API_KEY']??'','m'=>'llama-3.3-70b','name'=>'Cerebras-Llama3.3'],
|
|
['u'=>'https://api.mistral.ai/v1/chat/completions','k'=>$env['MISTRAL_KEY']??'','m'=>'mistral-small-latest','name'=>'Mistral'],
|
|
];
|
|
$reply=''; $provider_used='';
|
|
foreach($providers as $p){
|
|
if(!$p['k'])continue;
|
|
$ch=curl_init($p['u']);
|
|
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>1,CURLOPT_POST=>1,CURLOPT_TIMEOUT=>15,
|
|
CURLOPT_HTTPHEADER=>['Content-Type: application/json','Authorization: Bearer '.$p['k']],
|
|
CURLOPT_POSTFIELDS=>json_encode(['model'=>$p['m'],'messages'=>$messages,'max_tokens'=>1200,'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.';
|
|
$history[]=['role'=>'user','content'=>$msg];
|
|
$history[]=['role'=>'assistant','content'=>$reply];
|
|
if($redis)$redis->setex("saas:$session",3600,json_encode(array_slice($history,-20)));
|
|
|
|
echo json_encode([
|
|
'response' => wave256_sanitize($reply),
|
|
'provider'=>$provider_used ?: 'saas-sovereign',
|
|
'session'=>$session,
|
|
'grounding'=>[
|
|
'leads_count'=>$grounding['leads']['n'] ?? null,
|
|
'industries_count'=>count($grounding['industries'] ?? []),
|
|
'solutions_count'=>count($grounding['top_solutions'] ?? []),
|
|
'wave'=>253,
|
|
]
|
|
]);
|