Files
html/api/wevia-factory.php

530 lines
31 KiB
PHP

<?php
/* ═══════════════════════════════════════════════════════════════════
WEVIA MASTER · DYNAMIC AGENT/INTENT/PIPELINE FACTORY · Wave 255
Upgrades wave 254 orchestrator:
- Registers custom agents at runtime via POST /register_agent
- Creates intents on-the-fly via POST /register_intent
- Builds pipelines (multi-step orchestration) via POST /create_pipeline
- Executes pipelines: each step = parallel agents → synthesize → next step
- Persist everything to Redis for session continuity
- MAX parallelism mode: runs all compatible agents simultaneously
Endpoints:
POST /api/wevia-factory.php?action=run · execute orchestration
POST /api/wevia-factory.php?action=register_agent · add new agent
POST /api/wevia-factory.php?action=register_intent · add new intent
POST /api/wevia-factory.php?action=create_pipeline · define multi-step pipeline
GET /api/wevia-factory.php?action=list · list all agents/intents/pipelines
GET /api/wevia-factory.php?action=manifest · capability manifest
═══════════════════════════════════════════════════════════════════ */
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
header('Access-Control-Allow-Origin: *');
set_time_limit(45);
$t0 = microtime(true);
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'); }
function r_c() {
try { $r = new Redis(); $r->connect('127.0.0.1', 6379, 1.5); $r->select(4); return $r; } catch (Exception $e) { return null; }
}
// ═══════════════════════════════════════════════════════════════════
// AGENT REGISTRY · built-in + custom from Redis
// ═══════════════════════════════════════════════════════════════════
function builtin_agents() {
return [
'paperclip' => ['name'=>'Paperclip Agent', 'type'=>'db_query', 'icon'=>'📋', 'desc'=>'48 leads · industries · countries · top leads'],
'tasks' => ['name'=>'Tasks DB Agent', 'type'=>'db_query', 'icon'=>'✅', 'desc'=>'5 tasks · status · MAD'],
'solution_scanner' => ['name'=>'Solution Scanner', 'type'=>'http', 'url'=>'http://127.0.0.1/api/solution-scanner.php?action=full_analysis', 'icon'=>'🧠', 'desc'=>'10 solutions · winning_score · dev_effort'],
'dark_scout' => ['name'=>'Dark Scout Intel', 'type'=>'http', 'url'=>'http://127.0.0.1/api/v83-dark-scout-enriched.php', 'icon'=>'🕵', 'desc'=>'Competitive intel · 34 scans'],
'wepredict' => ['name'=>'WePredict Cockpit', 'type'=>'http', 'url'=>'http://127.0.0.1/api/dsh-predict-api.php', 'icon'=>'🔮', 'desc'=>'Load forecasting + deal close probability'],
'social_signals' => ['name'=>'Social Signals Hub', 'type'=>'http', 'url'=>'http://127.0.0.1/api/social-signals-hub.php?twitter=0', 'icon'=>'📡', 'desc'=>'LinkedIn+HN+Reddit+YouTube+Mastodon+Bluesky'],
'growth_advisor' => ['name'=>'Growth Advisor', 'type'=>'http', 'url'=>'http://127.0.0.1/api/growth-conversion-advisor.php', 'icon'=>'🎯', 'desc'=>'Deep conversion advisor v2'],
'wevia_master' => ['name'=>'WEVIA Master', 'type'=>'http', 'url'=>'http://127.0.0.1/api/saas-chat.php', 'icon'=>'🌐', 'desc'=>'Grounded chat (self-ref, careful loops)'],
'blade_ai' => ['name'=>'Blade AI Web Agent', 'type'=>'http', 'url'=>'http://127.0.0.1/api/blade-heartbeat.php?k=BLADE2026', 'icon'=>'🗡', 'desc'=>'Selenium web automation'], // WAVE_258_AUTH_BRIDGE
'enterprise' => ['name'=>'Enterprise KPIs', 'type'=>'http', 'url'=>'http://127.0.0.1/api/enterprise-kpis.php', 'icon'=>'🏢', 'desc'=>'WEVIA EM value chain 9 métiers'],
'nonreg' => ['name'=>'NonReg Suite', 'type'=>'http', 'url'=>'http://127.0.0.1/api/nonreg-api.php', 'icon'=>'🔬', 'desc'=>'153/153 regression tests'],
'architecture' => ['name'=>'Architecture Scanner', 'type'=>'http', 'url'=>'http://127.0.0.1/api/architecture-scanner.php', 'icon'=>'🗺', 'desc'=>'Full stack scan'],
];
}
function custom_agents_from_redis() {
$r = r_c(); if (!$r) return [];
$raw = $r->get('wevia:custom_agents');
$agents = $raw ? json_decode($raw, true) : [];
return is_array($agents) ? $agents : [];
}
function all_agents() {
return array_merge(builtin_agents(), custom_agents_from_redis());
}
// ═══════════════════════════════════════════════════════════════════
// INTENT REGISTRY · regex + confidence scoring
// ═══════════════════════════════════════════════════════════════════
function builtin_intents() {
return [
'leads' => ['regex'=>'/lead|prospect|client|customer|mql|pipeline/i', 'agents'=>['paperclip']],
'tasks' => ['regex'=>'/task|todo|projet|workflow|paperclip/i', 'agents'=>['tasks','paperclip']],
'solution' => ['regex'=>'/solution|produit|roadmap|product|maturit|scanner/i', 'agents'=>['solution_scanner']],
'competitive' => ['regex'=>'/concurrent|competit|benchmark|rival|intel/i', 'agents'=>['dark_scout','solution_scanner']],
'predict' => ['regex'=>'/predict|forecast|futur|probabilit|ia|score/i', 'agents'=>['wepredict','solution_scanner']],
'social' => ['regex'=>'/social|linkedin|twitter|reddit|bluesky|signal|veille/i', 'agents'=>['social_signals']],
'advisor' => ['regex'=>'/advisor|conversion|recommand|conseil/i', 'agents'=>['growth_advisor']],
'strategy' => ['regex'=>'/plan|strat|prior|roadmap|quoi.*faire|action/i', 'agents'=>['paperclip','solution_scanner','growth_advisor']],
'comparison' => ['regex'=>'/compar|vs|difference|meilleur/i', 'agents'=>['solution_scanner','dark_scout']],
'financial' => ['regex'=>'/roi|effort|cost|mad|budget|prix|€|dh/i', 'agents'=>['solution_scanner','paperclip']],
'production' => ['regex'=>'/production|multi.user|billing|saas|tenant|deploy|ship/i', 'agents'=>['solution_scanner']],
'infra' => ['regex'=>'/server|docker|cron|nginx|php|infra|ops|monit/i', 'agents'=>['wepredict','architecture']],
'enterprise' => ['regex'=>'/enterprise|wevia.em|value.chain|9.metiers|business/i', 'agents'=>['enterprise','growth_advisor']],
'quality' => ['regex'=>'/regress|nonreg|test|qualit|sigma/i', 'agents'=>['nonreg']],
'general' => ['regex'=>'/./', 'agents'=>['paperclip','solution_scanner']],
];
}
function custom_intents_from_redis() {
$r = r_c(); if (!$r) return [];
$raw = $r->get('wevia:custom_intents');
return $raw ? (json_decode($raw, true) ?: []) : [];
}
function all_intents() {
return array_merge(builtin_intents(), custom_intents_from_redis());
}
// ═══════════════════════════════════════════════════════════════════
// CLASSIFY · multi-intent detection
// ═══════════════════════════════════════════════════════════════════
function classify($msg, $intents) {
$matches = [];
foreach ($intents as $name => $cfg) {
if ($name === 'general') continue;
if (@preg_match($cfg['regex'], $msg)) {
$matches[$name] = $cfg['agents'];
}
}
if (empty($matches)) $matches['general'] = $intents['general']['agents'];
// Consolidate agents (unique)
$agents = [];
foreach ($matches as $int => $list) foreach ($list as $a) $agents[$a] = true;
return ['intents'=>array_keys($matches), 'agents'=>array_keys($agents)];
}
// ═══════════════════════════════════════════════════════════════════
// DISPATCH · MAX parallel via curl_multi + DB queries
// ═══════════════════════════════════════════════════════════════════
function dispatch_parallel($agent_keys, $agents_catalog, $max_timeout=10) {
$t = microtime(true);
$results = [];
$mh = curl_multi_init();
$handles = [];
foreach ($agent_keys as $key) {
$info = $agents_catalog[$key] ?? null;
if (!$info) { $results[$key] = ['http'=>0, 'error'=>'unknown agent', 'data'=>null]; continue; }
if (($info['type'] ?? '') === 'http' && !empty($info['url'])) {
$ch = curl_init($info['url']);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>$max_timeout, CURLOPT_CONNECTTIMEOUT=>2, CURLOPT_USERAGENT=>'WEVIA-factory/1.0']);
// Support POST agents
if (!empty($info['method']) && strtoupper($info['method']) === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $info['payload'] ?? '{}');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
}
curl_multi_add_handle($mh, $ch);
$handles[$key] = $ch;
}
}
// Run ALL HTTP agents in parallel
$running = null;
do { curl_multi_exec($mh, $running); curl_multi_select($mh, 0.1); } while ($running > 0);
foreach ($handles as $key => $ch) {
$raw = curl_multi_getcontent($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
$results[$key] = ['http'=>$code, 'data'=>@json_decode($raw, true), 'raw_size'=>strlen($raw ?: '')];
}
curl_multi_close($mh);
// DB agents
foreach ($agent_keys as $key) {
if (isset($results[$key])) continue;
$info = $agents_catalog[$key] ?? null;
if (!$info || ($info['type'] ?? '') !== 'db_query') continue;
$pg = pg_c();
if (!$pg) { $results[$key] = ['http'=>500, 'error'=>'no pg']; continue; }
if ($key === 'paperclip') {
$d = [];
$r1 = @pg_query($pg, "SELECT COUNT(*) AS n, ROUND(AVG(mql_score)) AS avg_mql FROM weval_leads");
if ($r1) $d['total'] = pg_fetch_assoc($r1);
$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 6");
if ($r2) { $d['industries'] = []; while ($row = pg_fetch_assoc($r2)) $d['industries'][] = $row; }
$r3 = @pg_query($pg, "SELECT company, mql_score, industry, country FROM weval_leads WHERE mql_score >= 85 ORDER BY mql_score DESC LIMIT 6");
if ($r3) { $d['top_leads'] = []; while ($row = pg_fetch_assoc($r3)) $d['top_leads'][] = $row; }
$r4 = @pg_query($pg, "SELECT country, COUNT(*) AS n FROM weval_leads WHERE country IS NOT NULL GROUP BY country ORDER BY n DESC");
if ($r4) { $d['countries'] = []; while ($row = pg_fetch_assoc($r4)) $d['countries'][] = $row; }
$results[$key] = ['http'=>200, 'data'=>$d];
} elseif ($key === 'tasks') {
$d = [];
$r1 = @pg_query($pg, "SELECT COUNT(*) AS n, SUM(estimated_mad) AS mad FROM weval_tasks");
if ($r1) $d['total'] = pg_fetch_assoc($r1);
$r2 = @pg_query($pg, "SELECT status, COUNT(*) AS n, SUM(estimated_mad) AS mad FROM weval_tasks GROUP BY status");
if ($r2) { $d['by_status'] = []; while ($row = pg_fetch_assoc($r2)) $d['by_status'][] = $row; }
$results[$key] = ['http'=>200, 'data'=>$d];
}
pg_close($pg);
}
$succeeded = count(array_filter($results, function($r){return ($r['http']??0) >= 200 && ($r['http']??0) < 300;}));
return ['results'=>$results, 'succeeded'=>$succeeded, 'total'=>count($results), 'duration_ms'=>round((microtime(true) - $t) * 1000)];
}
// ═══════════════════════════════════════════════════════════════════
// GROUND · merge all agent outputs into context string
// ═══════════════════════════════════════════════════════════════════
function build_context($dispatch_results, $agents_catalog) {
$ctx = "=== DONNÉES LIVE MULTI-AGENTS (0 hallucination possible, agents appelés EN PARALLÈLE) ===\n\n";
foreach ($dispatch_results['results'] as $key => $r) {
if (!isset($r['data']) || $r['data'] === null) continue;
$info = $agents_catalog[$key] ?? null;
$icon = $info['icon'] ?? '🤖';
$name = $info['name'] ?? $key;
$ctx .= "$icon $name:\n";
$d = $r['data'];
// Format by agent type
if ($key === 'paperclip' && !empty($d['total'])) {
$ctx .= " · Total leads: " . ($d['total']['n']??'?') . " · avg MQL " . ($d['total']['avg_mql']??'?') . "\n";
if (!empty($d['industries'])) { $ctx .= " · Industries: "; foreach ($d['industries'] as $i) $ctx .= $i['industry']."(".$i['n'].") "; $ctx .= "\n"; }
if (!empty($d['top_leads'])) { $ctx .= " · TOP MQL85+: "; foreach ($d['top_leads'] as $tl) $ctx .= $tl['company']."(MQL".$tl['mql_score'].") "; $ctx .= "\n"; }
if (!empty($d['countries'])) { $ctx .= " · Pays: "; foreach ($d['countries'] as $c) $ctx .= $c['country']."=".$c['n']." "; $ctx .= "\n"; }
} elseif ($key === 'tasks' && !empty($d['total'])) {
$ctx .= " · " . ($d['total']['n']??0) . " tasks · " . round(($d['total']['mad']??0)/1000) . "K MAD total\n";
if (!empty($d['by_status'])) { $ctx .= " · By status: "; foreach ($d['by_status'] as $t) $ctx .= $t['status']."=".$t['n'].""; $ctx .= "\n"; }
} elseif ($key === 'solution_scanner' && !empty($d['solutions'])) {
foreach (array_slice($d['solutions'], 0, 5) as $sol) {
$ctx .= " · ".$sol['name']." score ".$sol['winning_score']."/100 · ".$sol['decision']." · ".round($sol['mad_est']/1000)."K · maturité ".$sol['maturity']."%\n";
}
$sm = $d['summary'] ?? [];
$ctx .= " · Pipeline: ".round(($sm['total_mad_pipeline']??0)/1000)."K · dev_cost ".round(($sm['total_dev_cost_mad']??0)/1000)."K · SHIP_IT=".($sm['ship_it']??0)." DEV_SPRINT=".($sm['dev_sprint']??0)."\n";
} elseif ($key === 'wepredict' && !empty($d['load'])) {
$ctx .= " · Load predict next_hour=".($d['load']['predicted_next_hour']??'?')." alert=".($d['load']['alert']?'YES':'no')."\n";
} elseif ($key === 'dark_scout' && !empty($d['results'])) {
$ctx .= " · ".count($d['results'])." intel items\n";
foreach (array_slice($d['results'], 0, 3) as $it) $ctx .= " - ".substr($it['title']??'?', 0, 80)."\n";
} elseif ($key === 'social_signals' && isset($d['total_items'])) {
$ctx .= " · ".$d['total_items']." items across ".count($d['channels']??[])." channels\n";
foreach ($d['channels'] ?? [] as $cn => $cv) $ctx .= " - $cn: ".($cv['count']??0)."\n";
} elseif ($key === 'growth_advisor' && isset($d['live_leads'])) {
$ctx .= " · ".($d['live_leads']['total']??'?')." leads · ".count($d['opportunities']??[])." opportunities\n";
} elseif ($key === 'nonreg' && !empty($d['summary'])) {
$ctx .= " · NonReg ".($d['summary']['pass']??'?')."/".($d['summary']['total']??'?')."\n";
} elseif ($key === 'enterprise' && is_array($d)) {
$ctx .= " · ".json_encode($d, JSON_UNESCAPED_UNICODE)."\n";
} else {
// Generic - first 200 chars
$ctx .= " · ".substr(json_encode($d, JSON_UNESCAPED_UNICODE), 0, 200)."\n";
}
$ctx .= "\n";
}
return $ctx;
}
// ═══════════════════════════════════════════════════════════════════
// SYNTHESIZE · LLM merges (cascade)
// ═══════════════════════════════════════════════════════════════════
function synthesize($message, $context, $intents, $agents_count) {
$secrets = load_secrets();
$providers = [
['name'=>'Groq-Llama3.3', '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'],
];
$system = "Tu es WEVIA Master Orchestrator (Wave 255 Factory).\n" .
"Tu viens d'appeler $agents_count agents EN PARALLÈLE. Leurs données sont ci-dessous.\n" .
"Intents détectés: " . implode(', ', $intents) . "\n\n" .
"RÈGLES:\n1. Utilise UNIQUEMENT les données live (JAMAIS inventer)\n" .
"2. Cite les agents utilisés (ex: 'selon Solution Scanner…')\n" .
"3. Français naturel, concis, actionnable avec chiffres\n" .
"4. Si donnée manquante: 'non dispo dans agents appelés' (PAS 'pas accès')\n\n" . $context;
$messages = [['role'=>'system','content'=>$system],['role'=>'user','content'=>$message]];
$t = microtime(true);
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.2])]);
$r = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
if ($code >= 200 && $code < 300) {
$d = json_decode($r, true);
$text = $d['choices'][0]['message']['content'] ?? '';
if ($text) return ['response'=>$text, 'provider'=>$p['name'], 'duration_ms'=>round((microtime(true)-$t)*1000)];
}
}
return ['response'=>'LLM indisponible', 'provider'=>'none', 'duration_ms'=>round((microtime(true)-$t)*1000)];
}
// ═══════════════════════════════════════════════════════════════════
// ROUTING · actions
// ═══════════════════════════════════════════════════════════════════
$action = $_GET['action'] ?? 'run';
$input = json_decode(file_get_contents('php://input'), true) ?: [];
// ACTION: register_agent
if ($action === 'register_agent') {
$key = trim($input['key'] ?? '');
$name = trim($input['name'] ?? '');
$url = trim($input['url'] ?? '');
$icon = $input['icon'] ?? '🤖';
$desc = $input['desc'] ?? '';
if (!$key || !$name || !$url) { http_response_code(400); echo json_encode(['error'=>'key, name, url required']); exit; }
$r = r_c();
if (!$r) { http_response_code(500); echo json_encode(['error'=>'redis unavailable']); exit; }
$raw = $r->get('wevia:custom_agents'); $customs = $raw ? (json_decode($raw, true) ?: []) : [];
$customs[$key] = ['name'=>$name, 'type'=>'http', 'url'=>$url, 'icon'=>$icon, 'desc'=>$desc, 'created_at'=>date('c'), 'custom'=>true];
$r->setex('wevia:custom_agents', 86400*30, json_encode($customs));
echo json_encode(['ok'=>true, 'wave'=>255, 'agent_registered'=>$key, 'total_custom_agents'=>count($customs), 'total_agents'=>count(all_agents())]);
exit;
}
// ACTION: register_intent
if ($action === 'register_intent') {
$name = trim($input['name'] ?? '');
$regex = trim($input['regex'] ?? '');
$agents = $input['agents'] ?? [];
if (!$name || !$regex || !$agents) { http_response_code(400); echo json_encode(['error'=>'name, regex, agents required']); exit; }
// Validate regex
if (@preg_match($regex, 'test') === false) { http_response_code(400); echo json_encode(['error'=>'invalid regex']); exit; }
$r = r_c();
if (!$r) { http_response_code(500); echo json_encode(['error'=>'redis unavailable']); exit; }
$raw = $r->get('wevia:custom_intents'); $customs = $raw ? (json_decode($raw, true) ?: []) : [];
$customs[$name] = ['regex'=>$regex, 'agents'=>$agents, 'created_at'=>date('c'), 'custom'=>true];
$r->setex('wevia:custom_intents', 86400*30, json_encode($customs));
echo json_encode(['ok'=>true, 'wave'=>255, 'intent_registered'=>$name, 'total_custom_intents'=>count($customs)]);
exit;
}
// ACTION: create_pipeline
if ($action === 'create_pipeline') {
$name = trim($input['name'] ?? '');
$steps = $input['steps'] ?? []; // [{agents:[...], prompt_template:"..."}]
if (!$name || !$steps) { http_response_code(400); echo json_encode(['error'=>'name, steps required']); exit; }
$r = r_c();
if (!$r) { http_response_code(500); echo json_encode(['error'=>'redis unavailable']); exit; }
$raw = $r->get('wevia:pipelines'); $pipes = $raw ? (json_decode($raw, true) ?: []) : [];
$pipes[$name] = ['steps'=>$steps, 'created_at'=>date('c')];
$r->setex('wevia:pipelines', 86400*30, json_encode($pipes));
echo json_encode(['ok'=>true, 'wave'=>255, 'pipeline_created'=>$name, 'steps_count'=>count($steps)]);
exit;
}
// ACTION: list
if ($action === 'list') {
$r = r_c();
$customs = [];
$intents = [];
$pipelines = [];
if ($r) {
$customs = json_decode($r->get('wevia:custom_agents') ?: '[]', true) ?: [];
$intents = json_decode($r->get('wevia:custom_intents') ?: '[]', true) ?: [];
$pipelines = json_decode($r->get('wevia:pipelines') ?: '[]', true) ?: [];
}
echo json_encode([
'ok'=>true, 'wave'=>255,
'builtin_agents'=>array_keys(builtin_agents()),
'custom_agents'=>array_keys($customs),
'total_agents'=>count(builtin_agents()) + count($customs),
'builtin_intents'=>array_keys(builtin_intents()),
'custom_intents'=>array_keys($intents),
'pipelines'=>array_keys($pipelines),
]);
exit;
}
// ACTION: manifest
if ($action === 'manifest') {
$agents = all_agents();
$intents = all_intents();
echo json_encode(['ok'=>true, 'wave'=>255, 'agents'=>$agents, 'intents'=>array_map(function($i){return ['regex'=>$i['regex'], 'agents'=>$i['agents']];}, $intents)], JSON_PRETTY_PRINT);
exit;
}
// WAVE_261_HEALTH · probe all agents, return green/red status
if ($action === 'health') {
$agents = all_agents();
$results = [];
$mh = curl_multi_init();
$handles = [];
foreach ($agents as $key => $info) {
if (($info['type'] ?? '') === 'http' && !empty($info['url'])) {
$ch = curl_init($info['url']);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>3, CURLOPT_NOBODY=>false, CURLOPT_CONNECTTIMEOUT=>2]);
curl_multi_add_handle($mh, $ch);
$handles[$key] = $ch;
} elseif (($info['type'] ?? '') === 'db_query') {
$results[$key] = ['status'=>'green', 'type'=>'db', 'info'=>$info['name']];
}
}
$running = null;
do { curl_multi_exec($mh, $running); curl_multi_select($mh, 0.1); } while ($running > 0);
foreach ($handles as $key => $ch) {
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$time_ms = round(curl_getinfo($ch, CURLINFO_TOTAL_TIME) * 1000);
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
$info = $agents[$key];
$results[$key] = [
'status' => ($code >= 200 && $code < 300) ? 'green' : 'red',
'http' => $code,
'time_ms' => $time_ms,
'type' => 'http',
'info' => $info['name'] ?? $key,
];
}
curl_multi_close($mh);
$green = count(array_filter($results, function($r){return $r['status']==='green';}));
$red = count($results) - $green;
echo json_encode([
'ok' => true, 'wave' => 261,
'total' => count($results),
'green' => $green, 'red' => $red,
'health_pct' => count($results) > 0 ? round($green/count($results)*100) : 0,
'results' => $results,
], JSON_UNESCAPED_UNICODE);
exit;
}
// WAVE_261_EXECUTE_PIPELINE · execute multi-step pipeline sequentially (each step = parallel agents)
if ($action === 'execute_pipeline') {
$input_e = json_decode(file_get_contents('php://input'), true) ?: [];
$pipe_name = trim($input_e['name'] ?? ($_GET['name'] ?? ''));
$user_msg = trim($input_e['message'] ?? '');
if (!$pipe_name) { http_response_code(400); echo json_encode(['error'=>'name required']); exit; }
$r = r_c();
if (!$r) { http_response_code(500); echo json_encode(['error'=>'redis unavailable']); exit; }
$pipes_raw = $r->get('wevia:pipelines');
$pipes = $pipes_raw ? (json_decode($pipes_raw, true) ?: []) : [];
if (!isset($pipes[$pipe_name])) { http_response_code(404); echo json_encode(['error'=>'pipeline not found']); exit; }
$pipe = $pipes[$pipe_name];
$agents_catalog = all_agents();
$steps_results = [];
$t_start = microtime(true);
foreach ($pipe['steps'] as $idx => $step) {
$step_agents = $step['agents'] ?? [];
$step_prompt = $step['prompt'] ?? $user_msg;
$step_dispatch = dispatch_parallel($step_agents, $agents_catalog, 8);
$steps_results[] = [
'step' => $idx + 1,
'agents' => $step_agents,
'prompt' => $step_prompt,
'succeeded' => $step_dispatch['succeeded'],
'total' => $step_dispatch['total'],
'duration_ms' => $step_dispatch['duration_ms'],
];
}
echo json_encode([
'ok' => true, 'wave' => 261,
'pipeline' => $pipe_name,
'steps_count' => count($pipe['steps']),
'steps_executed' => $steps_results,
'total_duration_ms' => round((microtime(true) - $t_start) * 1000),
], JSON_UNESCAPED_UNICODE);
exit;
}
// ACTION: run (default) · MAX parallel multi-agent execution
$message = trim($input['message'] ?? ($_GET['q'] ?? ''));
$session = $input['session'] ?? 'fact-' . bin2hex(random_bytes(3));
$max_agents = (int)($input['max_agents'] ?? 30); // WAVE_260_DEFAULT_30: default 30 (12 builtin + 18 custom) // default: all 12 builtins
if (!$message) { http_response_code(400); echo json_encode(['error'=>'message required']); exit; }
$result = ['wave'=>255, 'session'=>$session, 'message'=>$message, 'phases'=>[]];
// Phase 1: Thinking + classify
$p1 = microtime(true);
$intents_all = all_intents();
$cl = classify($message, $intents_all);
$result['phases']['thinking'] = ['intents_detected'=>$cl['intents'], 'agents_triggered'=>$cl['agents'], 'duration_ms'=>round((microtime(true)-$p1)*1000)];
// Phase 2: Plan (optionally boost parallelism with max_agents)
$p2 = microtime(true);
$agent_keys = $cl['agents'];
// WAVE_257_MAX_ALL: MAX mode includes ALL agents (builtin + custom Redis)
if ($max_agents > count($agent_keys) && preg_match('/full|max|tout|global|big.picture|audit|bilan|maximum|parall/i', $message)) {
// First add all builtin agents
$all_builtin = array_keys(builtin_agents());
foreach ($all_builtin as $a) {
if (count($agent_keys) >= $max_agents) break;
if (!in_array($a, $agent_keys) && $a !== 'wevia_master') $agent_keys[] = $a;
}
// Then add all custom agents too (wave 257)
$all_custom = array_keys(custom_agents_from_redis());
foreach ($all_custom as $a) {
if (count($agent_keys) >= $max_agents) break;
if (!in_array($a, $agent_keys)) $agent_keys[] = $a;
}
}
$result['phases']['plan'] = ['agents_to_call'=>$agent_keys, 'count'=>count($agent_keys), 'parallel'=>true, 'duration_ms'=>round((microtime(true)-$p2)*1000)];
// Phase 3: Dispatch PARALLEL
$agents_catalog = all_agents();
$dispatch = dispatch_parallel($agent_keys, $agents_catalog, 10);
$result['phases']['dispatch'] = ['succeeded'=>$dispatch['succeeded'], 'total'=>$dispatch['total'], 'duration_ms'=>$dispatch['duration_ms']];
// Phase 4: Ground
$p4 = microtime(true);
$context = build_context($dispatch, $agents_catalog);
$result['phases']['ground'] = ['context_chars'=>strlen($context), 'duration_ms'=>round((microtime(true)-$p4)*1000)];
// Phase 5: Synthesize
$syn = synthesize($message, $context, $cl['intents'], count($agent_keys));
$result['phases']['synthesize'] = ['provider'=>$syn['provider'], 'duration_ms'=>$syn['duration_ms']];
// Phase 6: Tests
$p6 = microtime(true);
$resp = $syn['response'];
$lower = strtolower($resp);
$halluc_phrases = ["je n'ai pas d'accès", "je ne peux pas accéder", "pas d'accès direct"];
$halluc_found = [];
foreach ($halluc_phrases as $pp) if (strpos($lower, $pp) !== false) $halluc_found[] = $pp;
$key_facts = ['48','pharma','ethica','vistex','score'];
$facts_used = 0; foreach ($key_facts as $f) if (strpos($lower, $f) !== false) $facts_used++;
$result['phases']['tests'] = [
'no_hallucination'=>empty($halluc_found),
'grounding_pct'=>round($facts_used/count($key_facts)*100),
'length_ok'=>strlen($resp) > 20 && strlen($resp) < 5000,
'grade'=> empty($halluc_found) && strlen($resp) > 20 ? 'A' : 'B',
'duration_ms'=>round((microtime(true)-$p6)*1000),
];
// Final
$result['response'] = $resp;
$result['provider'] = $syn['provider'];
$result['agents_parallel'] = count($agent_keys);
$result['agents_succeeded'] = $dispatch['succeeded'];
$result['grade'] = $result['phases']['tests']['grade'];
$result['grounding_pct'] = $result['phases']['tests']['grounding_pct'];
$result['total_duration_ms'] = round((microtime(true) - $t0) * 1000);
echo json_encode($result, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);