Files
html/api/saas-chat.php
opus 855c28d9b9
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
auto-sync-0445
2026-04-22 04:45:03 +02:00

172 lines
9.5 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);
$msg=$input['message']??$_POST['message']??'';
$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,
]
]);