'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'); } // ═══════════════════════════════════════════════════════════════════ // PHASE 1: THINKING · intent classification // ═══════════════════════════════════════════════════════════════════ function phase_thinking($msg) { $lower = strtolower($msg); $intents = []; // Data queries if (preg_match('/lead|prospect|client|pipeline/i', $msg)) $intents[] = 'paperclip'; if (preg_match('/solution|produit|scanner|roadmap|dev.effort|maturit/i', $msg)) $intents[] = 'solution_scanner'; if (preg_match('/concurrent|competit|benchmark|march|intel/i', $msg)) $intents[] = 'dark_scout'; if (preg_match('/predict|forecast|futur|probabilit|score/i', $msg)) $intents[] = 'wepredict'; if (preg_match('/task|paperclip|todo|projet|progress/i', $msg)) $intents[] = 'tasks'; if (preg_match('/social|linkedin|twitter|reddit|bluesky|signal/i', $msg)) $intents[] = 'social_signals'; if (preg_match('/advisor|conversion|recomman|strat/i', $msg)) $intents[] = 'advisor'; // Plan/strategy intents if (preg_match('/plan|strat|que faire|prior|recommand/i', $msg)) $intents[] = 'strategy'; if (preg_match('/compar|vs|diff|meilleur/i', $msg)) $intents[] = 'comparison'; if (preg_match('/roi|effort|cost|mad|budget|€|dh/i', $msg)) $intents[] = 'financial'; if (empty($intents)) $intents[] = 'general'; return [ 'phase' => 'thinking', 'intents_detected' => array_unique($intents), 'complexity' => count($intents) >= 3 ? 'high' : (count($intents) >= 2 ? 'medium' : 'low'), 'duration_ms' => 0, ]; } // ═══════════════════════════════════════════════════════════════════ // PHASE 2: PLAN · which agents to call in parallel // ═══════════════════════════════════════════════════════════════════ function phase_plan($intents) { $agent_map = [ 'paperclip' => ['name'=>'Paperclip Agent', 'type'=>'db_query', 'icon'=>'📋'], 'solution_scanner' => ['name'=>'Solution Scanner', 'url'=>'http://127.0.0.1/api/solution-scanner.php?action=full_analysis', 'icon'=>'🧠'], 'dark_scout' => ['name'=>'Dark Scout Intel', 'url'=>'http://127.0.0.1/api/v83-dark-scout-enriched.php', 'icon'=>'🕵'], 'wepredict' => ['name'=>'WePredict Cockpit', 'url'=>'http://127.0.0.1/api/dsh-predict-api.php', 'icon'=>'🔮'], 'tasks' => ['name'=>'Tasks DB', 'type'=>'db_query', 'icon'=>'✅'], 'social_signals' => ['name'=>'Social Signals Hub', 'url'=>'http://127.0.0.1/api/social-signals-hub.php', 'icon'=>'📡'], 'advisor' => ['name'=>'Growth Advisor', 'url'=>'http://127.0.0.1/api/growth-conversion-advisor.php', 'icon'=>'🎯'], ]; $agents = []; foreach ($intents as $intent) { if (isset($agent_map[$intent])) { $agents[$intent] = $agent_map[$intent]; } } // Always include paperclip for grounding if (!isset($agents['paperclip'])) $agents['paperclip'] = $agent_map['paperclip']; return [ 'phase' => 'plan', 'agents_to_call' => array_keys($agents), 'agents_count' => count($agents), 'parallel' => true, 'agent_details' => $agents, ]; } // ═══════════════════════════════════════════════════════════════════ // PHASE 3: DISPATCH · parallel curl_multi + DB queries // ═══════════════════════════════════════════════════════════════════ function phase_dispatch($agents) { $t = microtime(true); $results = []; // HTTP agents via curl_multi (parallel) $mh = curl_multi_init(); $handles = []; foreach ($agents as $key => $info) { if (isset($info['url'])) { $ch = curl_init($info['url']); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>8, CURLOPT_CONNECTTIMEOUT=>2, CURLOPT_USERAGENT=>'WEVIA-multiagent/1.0']); curl_multi_add_handle($mh, $ch); $handles[$key] = $ch; } } $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 (Paperclip, Tasks) if (isset($agents['paperclip'])) { $pg = pg_c(); if ($pg) { $leads = ['total'=>0, 'top_industries'=>[], 'top_countries'=>[], 'top_leads'=>[], 'by_status'=>[]]; $r1 = @pg_query($pg, "SELECT COUNT(*) AS n, ROUND(AVG(mql_score)) AS avg_mql FROM weval_leads"); if ($r1) $leads['total'] = pg_fetch_assoc($r1); $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 LIMIT 6"); if ($r2) while ($row = pg_fetch_assoc($r2)) $leads['top_industries'][] = $row; $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) while ($row = pg_fetch_assoc($r3)) $leads['top_countries'][] = $row; $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 5"); if ($r4) while ($row = pg_fetch_assoc($r4)) $leads['top_leads'][] = $row; $r5 = @pg_query($pg, "SELECT status, COUNT(*) AS n FROM weval_leads GROUP BY status"); if ($r5) while ($row = pg_fetch_assoc($r5)) $leads['by_status'][] = $row; pg_close($pg); $results['paperclip'] = ['http'=>200, 'data'=>$leads, 'raw_size'=>json_encode($leads)]; } } if (isset($agents['tasks'])) { $pg = pg_c(); if ($pg) { $tasks = ['total'=>0, 'by_status'=>[]]; $r1 = @pg_query($pg, "SELECT COUNT(*) AS n, SUM(estimated_mad) AS mad FROM weval_tasks"); if ($r1) $tasks['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) while ($row = pg_fetch_assoc($r2)) $tasks['by_status'][] = $row; pg_close($pg); $results['tasks'] = ['http'=>200, 'data'=>$tasks]; } } return [ 'phase' => 'dispatch', 'results' => $results, 'agents_succeeded' => count(array_filter($results, function($r){return ($r['http']??0) >= 200 && ($r['http']??0) < 300;})), 'agents_failed' => count(array_filter($results, function($r){return ($r['http']??0) >= 400 || !($r['data']??null);})), 'duration_ms' => round((microtime(true) - $t) * 1000), ]; } // ═══════════════════════════════════════════════════════════════════ // PHASE 4: GROUND · build consolidated context // ═══════════════════════════════════════════════════════════════════ function phase_ground($dispatch_results) { $ctx = "DONNÉES LIVE WEVAL (agents appelés en parallèle, pas d'hallucination possible):\n\n"; $results = $dispatch_results['results'] ?? []; if (!empty($results['paperclip']['data'])) { $p = $results['paperclip']['data']; $t = $p['total'] ?? []; $ctx .= "📋 PAPERCLIP (48 leads DB):\n"; $ctx .= " · Total: " . ($t['n'] ?? '?') . " leads · avg MQL " . ($t['avg_mql'] ?? '?') . "\n"; if (!empty($p['top_industries'])) { $ctx .= " · Industries: "; foreach ($p['top_industries'] as $i) $ctx .= $i['industry'] . "(" . $i['n'] . ") "; $ctx .= "\n"; } if (!empty($p['top_leads'])) { $ctx .= " · TOP leads MQL85+: "; foreach ($p['top_leads'] as $tl) $ctx .= $tl['company'] . "(MQL" . $tl['mql_score'] . ") · "; $ctx .= "\n"; } } if (!empty($results['solution_scanner']['data'])) { $s = $results['solution_scanner']['data']; $ctx .= "\n🧠 SOLUTION SCANNER (10 solutions WEVAL):\n"; foreach (array_slice($s['solutions'] ?? [], 0, 5) as $sol) { $ctx .= " · " . $sol['name'] . " score " . $sol['winning_score'] . "/100 · " . $sol['decision'] . " · " . round($sol['mad_est']/1000) . "K MAD · maturité " . $sol['maturity'] . "%\n"; } $sm = $s['summary'] ?? []; $ctx .= " · Pipeline global: " . round(($sm['total_mad_pipeline'] ?? 0)/1000) . "K MAD · dev cost " . round(($sm['total_dev_cost_mad'] ?? 0)/1000) . "K · SHIP_IT=" . ($sm['ship_it']??0) . " DEV_SPRINT=" . ($sm['dev_sprint']??0) . "\n"; } if (!empty($results['wepredict']['data'])) { $w = $results['wepredict']['data']; $ctx .= "\n🔮 WEPREDICT: load predicted_next_hour=" . ($w['load']['predicted_next_hour'] ?? '?') . " alert=" . ($w['load']['alert'] ? 'YES' : 'no') . "\n"; } if (!empty($results['tasks']['data'])) { $t = $results['tasks']['data']; $ctx .= "\n✅ TASKS DB: " . ($t['total']['n'] ?? '?') . " tasks · " . round(($t['total']['mad'] ?? 0)/1000) . "K MAD total\n"; } if (!empty($results['dark_scout']['data'])) { $d = $results['dark_scout']['data']; $ctx .= "\n🕵 DARK SCOUT: " . count($d['results'] ?? []) . " intel items\n"; } if (!empty($results['social_signals']['data'])) { $s = $results['social_signals']['data']; $ctx .= "\n📡 SOCIAL SIGNALS: " . ($s['total_items'] ?? 0) . " items across " . count($s['channels'] ?? []) . " channels\n"; } return [ 'phase' => 'ground', 'context_chars' => strlen($ctx), 'context' => $ctx, ]; } // ═══════════════════════════════════════════════════════════════════ // PHASE 5: SYNTHESIZE · LLM merges agent outputs (cascade) // ═══════════════════════════════════════════════════════════════════ function phase_synthesize($message, $context, $intents) { $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-Llama3.3', '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_prompt = "Tu es WEVIA Master Orchestrator multi-agents de WEVAL Consulting Casablanca.\n\n" . "Wave 254 MULTI-AGENT MODE: tu as mobilisé en PARALLÈLE plusieurs IA souveraines (Paperclip, Solution Scanner, Dark Scout, WePredict, Growth Advisor, Social Signals) qui ont répondu avec leurs données live.\n\n" . "RÈGLES STRICTES:\n" . "1. Utilise UNIQUEMENT les données live ci-dessous (JAMAIS inventer)\n" . "2. Synthesize les outputs multi-agents en UNE réponse cohérente\n" . "3. Cite les agents utilisés (ex: 'selon Solution Scanner...', 'Paperclip indique...')\n" . "4. Langage naturel français, concis, actionnable\n" . "5. Intents détectés: " . implode(', ', $intents) . "\n" . "6. Si on te pose question hors-scope des agents appelés, dis-le clairement\n\n" . $context; $messages = [ ['role'=>'system', 'content'=>$system_prompt], ['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 [ 'phase' => 'synthesize', 'response' => $text, 'provider' => $p['name'], 'duration_ms' => round((microtime(true) - $t) * 1000), ]; } } } return ['phase' => 'synthesize', 'response' => 'Service LLM indisponible', 'provider' => 'none', 'duration_ms' => round((microtime(true) - $t) * 1000)]; } // ═══════════════════════════════════════════════════════════════════ // PHASE 6: TESTS · hallucination check // ═══════════════════════════════════════════════════════════════════ function phase_tests($response, $context) { $lower_resp = strtolower($response); $tests = []; // Anti-hallucination tests $hallucinate_phrases = ["je n'ai pas d'accès", "je ne peux pas accéder", "pas d'accès direct", "i don't have access", "je ne connais pas"]; $hallucinations = []; foreach ($hallucinate_phrases as $p) { if (strpos($lower_resp, $p) !== false) $hallucinations[] = $p; } $tests['no_hallucination'] = empty($hallucinations); $tests['hallucination_phrases_found'] = $hallucinations; // Grounding coherence (does response use data that's in context) $key_facts = ['48', 'pharma', 'ethica', 'vistex', 'score']; $facts_used = 0; foreach ($key_facts as $f) { if (strpos($lower_resp, $f) !== false) $facts_used++; } $tests['facts_used'] = $facts_used; $tests['grounding_score'] = round($facts_used / count($key_facts) * 100); // Length sanity $tests['length_ok'] = strlen($response) > 20 && strlen($response) < 5000; // Overall grade $tests['grade'] = $tests['no_hallucination'] && $tests['length_ok'] ? 'A' : 'B'; return [ 'phase' => 'tests', 'passed' => $tests['no_hallucination'] && $tests['length_ok'], 'tests' => $tests, ]; } // ═══════════════════════════════════════════════════════════════════ // EXECUTION (7-phase pattern CLAUDE) // ═══════════════════════════════════════════════════════════════════ $result = ['wave' => 254, 'session' => $session, 'message' => $message, 'phases' => []]; // Phase 1: Thinking $p1_start = microtime(true); $result['phases']['thinking'] = phase_thinking($message); $result['phases']['thinking']['duration_ms'] = round((microtime(true) - $p1_start) * 1000); // Phase 2: Plan $p2_start = microtime(true); $result['phases']['plan'] = phase_plan($result['phases']['thinking']['intents_detected']); $result['phases']['plan']['duration_ms'] = round((microtime(true) - $p2_start) * 1000); // Phase 3: Dispatch (PARALLEL) $result['phases']['dispatch'] = phase_dispatch($result['phases']['plan']['agent_details']); // Phase 4: Ground $p4_start = microtime(true); $result['phases']['ground'] = phase_ground($result['phases']['dispatch']); $result['phases']['ground']['duration_ms'] = round((microtime(true) - $p4_start) * 1000); // Phase 5: Synthesize $result['phases']['synthesize'] = phase_synthesize($message, $result['phases']['ground']['context'], $result['phases']['thinking']['intents_detected']); // Phase 6: Tests $p6_start = microtime(true); $response_text = $result['phases']['synthesize']['response'] ?? ''; $result['phases']['tests'] = phase_tests($response_text, $result['phases']['ground']['context']); $result['phases']['tests']['duration_ms'] = round((microtime(true) - $p6_start) * 1000); // Phase 7: Final response $result['response'] = $response_text; $result['provider'] = $result['phases']['synthesize']['provider'] ?? '?'; $result['agents_used'] = $result['phases']['plan']['agents_to_call'] ?? []; $result['agents_succeeded'] = $result['phases']['dispatch']['agents_succeeded'] ?? 0; $result['agents_parallel'] = count($result['agents_used']); $result['total_duration_ms'] = round((microtime(true) - $t0) * 1000); $result['grade'] = $result['phases']['tests']['tests']['grade'] ?? '?'; $result['grounding_score'] = $result['phases']['tests']['tests']['grounding_score'] ?? 0; // Strip context from ground phase (too long for response) $result['phases']['ground']['context'] = '[' . strlen($result['phases']['ground']['context']) . ' chars, not included]'; // Strip raw dispatch data (keep only summary) foreach ($result['phases']['dispatch']['results'] as $k => &$v) { unset($v['data']); unset($v['raw_size']); } unset($v); echo json_encode($result, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);