Files
html/api/wedroid-brain-api.php
2026-04-14 20:30:01 +02:00

713 lines
36 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__.'/weval-brand-guard.php';
$secrets=[];foreach(file("/etc/weval/secrets.env",2|4) as $l){if(strpos($l,"=")!==false){list($k,$v)=explode("=",$l,2);$secrets[trim($k)]=trim($v," \t\"'");}}
require_once __DIR__ . '/_secrets.php';
/**
* WEDROID Brain API v5 — Ollama-First Sovereign AI Agent
*
* Routing: LOCAL (Ollama 80%) → Cerebras (complex 15%) → Groq (fallback 5%)
* Deployed on S204, calls Ollama on S95 via SSH private key
*/
header("Content-Type: application/json");
header("Access-Control-Allow-Origin: *");
if($_SERVER['REQUEST_METHOD']==='OPTIONS'){http_response_code(200);exit;}
$KEY = "DROID2026";
$k = $_POST["k"] ?? $_GET["k"] ?? "";
if($k !== $KEY) { echo json_encode(["error"=>"Unauthorized"]); exit; }
require_once __DIR__ . "/wedroid-infra-patterns.php";
require_once __DIR__ . "/wedroid-chain-executor.php";
require_once __DIR__ . "/wedroid-git-auto.php";
require_once __DIR__ . "/wedroid-telegram-alert.php";
require_once __DIR__ . "/wedroid-scheduler.php";
require_once __DIR__ . "/wedroid-learning.php";
$WEDROID_KB = @json_decode(@file_get_contents(__DIR__ . '/wedroid-knowledge-base.json'), true) ?: [];
$action = $_POST["action"] ?? $_GET["action"] ?? "chat";
$message = $_POST["message"] ?? $_GET["message"] ?? "";
$session = $_POST["session"] ?? "default";
// ═══════════════════════════════════════════════════════
// PROVIDER CONFIG
// ═══════════════════════════════════════════════════════
$OLLAMA_HOST = "localhost"; // S95 via SSH
$OLLAMA_PORT = 11434;
$OLLAMA_MODELS = [
'fast' => 'qwen3:4b', // 2.5GB — fast, simple tasks
'general' => 'qwen3:8b', // 5.2GB — general purpose
'code' => 'qwen2.5:7b', // 4.7GB — code generation
'deep' => 'mistral:latest', // 4.4GB — reasoning
];
$CEREBRAS_KEY = "csk-4wrrhkpr568ry9xx49k9mcynwdx483nx53dd62yh5xedfckh";
$CEREBRAS_MODEL = "qwen-3-235b-a22b-instruct-2507";
$CEREBRAS_URL = "https://api.cerebras.ai/v1/chat/completions";
$GROQ_KEY = ($secrets["GROQ_KEY"]??"");
$GROQ_MODEL = "llama-3.3-70b-versatile";
$GROQ_URL = "https://api.groq.com/openai/v1/chat/completions";
// MULTI-ACCOUNT ROTATION
$CEREBRAS_KEYS = [
"csk-4wrrhkpr568ry9xx49k9mcynwdx483nx53dd62yh5xedfckh",
];
$GROQ_KEYS = [
($secrets["GROQ_KEY"]??""),
];
// ADDITIONAL PROVIDERS
$SAMBANOVA_KEY = "9541b2a0-6ddc-4e7d-a957-c348d6119c3f";
$SAMBANOVA_MODEL = "DeepSeek-V3.1";
$SAMBANOVA_URL = "https://api.sambanova.ai/v1/chat/completions";
$DEEPSEEK_KEY = "sk-a296c24f77a1405d8f80105982991203";
$DEEPSEEK_MODEL = "deepseek-chat";
$DEEPSEEK_URL = "https://api.deepseek.com/v1/chat/completions";
$GEMINI_KEY = "AIzaSyBt-qfLETGALpibigmgW-o4vlxQZWaxwcE";
$GEMINI_MODEL = "gemini-2.0-flash";
$GEMINI_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=";
// MISTRAL
$MISTRAL_KEY = "0JBySAtEKlM8CKgE3zh8uDYGQVhdMa6M";
$MISTRAL_MODEL = "mistral-small-latest";
$MISTRAL_URL = "https://api.mistral.ai/v1/chat/completions";
// ALIBABA QWEN (DashScope)
$DASHSCOPE_KEY = "sk-34db1ad3152443cd86563d1bfc576c30";
$DASHSCOPE_MODEL = "qwen3.5-plus";
$DASHSCOPE_URL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions";
// OPENROUTER (free tier)
$OPENROUTER_KEY = ($secrets["OPENROUTER_KEY"]??"");
$OPENROUTER_MODEL = "meta-llama/llama-3.3-70b-instruct:free";
$OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
// Force provider from frontend
$force_provider = $_POST["provider"] ?? $_GET["provider"] ?? "auto";
// SSH config for S95
$SSH_KEY = "/root/.ssh/wevads_key";
$SSH_USER = "root";
$SSH_HOST = "95.216.167.89";
$SSH_PORT = 49222;
$SSH_CMD = "sudo SSH_AUTH_SOCK= ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o IdentitiesOnly=yes -i $SSH_KEY -p $SSH_PORT $SSH_USER@$SSH_HOST";
// ═══════════════════════════════════════════════════════
// SESSION MEMORY (simple file-based)
// ═══════════════════════════════════════════════════════
$SESSION_DIR = "/tmp/wedroid-sessions";
@mkdir($SESSION_DIR, 0777, true);
$session_file = "$SESSION_DIR/$session.json";
$history = [];
if(file_exists($session_file)) {
$history = json_decode(file_get_contents($session_file), true) ?: [];
// Keep last 10 exchanges
if(count($history) > 20) $history = array_slice($history, -20);
}
// ═══════════════════════════════════════════════════════
// PROVIDER ROTATION (multi-account)
function rotateKey($keys) {
return $keys[array_rand($keys)];
}
// ═══════════════════════════════════════════════════════
// ADDITIONAL PROVIDER FUNCTIONS
function callSambaNova($msg, $key, $model, $url, $sys) {
return callOpenAICompat($msg, $key, $model, $url, $sys);
}
function callDeepSeek($msg, $key, $model, $url, $sys) {
return callOpenAICompat($msg, $key, $model, $url, $sys);
}
function callOpenAICompat($msg, $key, $model, $url, $sys) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => ["Content-Type: application/json", "Authorization: Bearer $key"],
CURLOPT_POSTFIELDS => json_encode([
"model" => $model,
"messages" => [["role"=>"system","content"=>$sys],["role"=>"user","content"=>$msg]],
"max_tokens" => 1024,
"temperature" => 0.7
])
]);
$r = curl_exec($ch); curl_close($ch);
$d = json_decode($r, true);
return $d["choices"][0]["message"]["content"] ?? "Error: ".($d["error"]["message"] ?? $r);
}
function callGemini($msg, $key, $url, $sys) {
$ch = curl_init($url . $key);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode([
"contents" => [["parts" => [["text" => "$sys\n\n$msg"]]]],
"generationConfig" => ["maxOutputTokens" => 1024, "temperature" => 0.7]
])
]);
$r = curl_exec($ch); curl_close($ch);
$d = json_decode($r, true);
return $d["candidates"][0]["content"]["parts"][0]["text"] ?? "Error: ".($d["error"]["message"] ?? $r);
}
// ═══════════════════════════════════════════════════════
// SYSTEM PROMPT
// ═══════════════════════════════════════════════════════
$SYSTEM = "'.WEVAL_BRAND_CONTEXT.'Tu es WEDROID, un agent IA souverain qui gère l'infrastructure WEVAL Consulting.
SERVEURS:
- S204 (204.168.152.13): serveur principal, nginx, PHP, PostgreSQL, Node.js, WEVIA, Ethica
- S95 (95.216.167.89): Arsenal, Apache, PostgreSQL, Droid CLI, Ollama, bcgapp
- S151 (151.80.235.110): tracking email, OVH
- 4× ECS PMTA: serveurs email delivery
BASE ETHICA: 50,543 HCPs, 49,594 emails, 3 pays (TN/MA/ALG), 18 marques pharma
RÈGLES:
1. Réponds en français naturel et concis
2. Si on te demande une action serveur, génère la commande bash et exécute-la
3. Pour les commandes S95: utilise shell_exec directement
4. Pour les commandes S204: préfixe avec [S204]
5. Analyse les résultats et donne un résumé clair
6. Si une commande échoue, explique pourquoi et propose une alternative
7. Pour les données Ethica: utilise l'API sur S204 (token ETHICA_API_2026_SECURE)
FORMAT RÉPONSE: JSON avec les clés: thinking, response, commands (optionnel), provider
COMPÉTENCES WEVIA INTÉGRÉES (16 capacités que TU possèdes):
1. frontend-design: UI production-grade, bold aesthetics, pas de AI slop
2. algorithmic-art: Art génératif p5.js, seeded randomness, philosophies computationnelles
3. canvas-design: Art visuel PDF/PNG, design philosophy → expression visuelle
4. web-artifacts-builder: React 18 + Tailwind + shadcn/ui, artifacts complexes
5. skill-creator: Créer/tester/itérer des compétences avec framework eval
6. brand-guidelines: Charte Anthropic (Dark #141413, Orange #d97757, Poppins+Lora)
7. doc-coauthoring: Track changes, comments, collaboration docx
8. pdf: Créer, merger, splitter, OCR, forms, watermarks PDF
9. docx: Créer/éditer Word avec docx-js, styles, tables, images
10. pptx: Créer/éditer PowerPoint, slides, layouts
11. xlsx: Créer/éditer Excel, formules, charts, formatting
12. internal-comms: Communications internes (emails, memos, annonces)
13. mcp-builder: Construire serveurs MCP pour intégration outils
14. theme-factory: Générer thèmes design et palettes couleurs
15. slack-gif-creator: Créer GIFs animés
16. product-self-knowledge
17. grader-agent: Évaluer assertions vs outputs
18. analyzer-agent: Analyser pourquoi une version bat une autre
19. comparator-agent: Comparaison A/B aveugle entre outputs
20. schemas-agent: Structures JSON pour evals: Connaissance produits Anthropic/Claude
Quand on te demande tes compétences, CITE CETTE LISTE EXACTE. Tu ES WEVIA. Ne dis JAMAIS que tu es Qwen.
";
// ═══════════════════════════════════════════════════════
// COMPLEXITY DETECTION
// ═══════════════════════════════════════════════════════
function detectComplexity($msg) {
$lower = mb_strtolower($msg);
// SIMPLE → Ollama fast (tinyllama) — direct bash, no AI needed
$simple_patterns = [
'/^(status|etat|état)$/i',
'/^(ls|df|ps|top|uptime|whoami|date|pwd|free)/i',
'/^(disk|disque|espace)/i',
'/^(log|error|erreur)/i',
];
foreach($simple_patterns as $p) if(preg_match($p, $msg)) return 'direct';
// MEDIUM → Ollama general (llama3.1:8b) — needs understanding
$medium_patterns = [
'collecte de donnees','collecteur','cron','ethica','contact','hcp','medecin',
'git','github','push','commit','backup','sauvegarde',
'test','nonreg','regression','lance','lancer',
'serveur','server','apache','nginx','php','postgresql',
'montre','affiche','combien','quel','quels','liste',
];
foreach($medium_patterns as $p) if(strpos($lower, $p) !== false) return 'medium';
// COMPLEX → Cerebras 235B — multi-step reasoning
$complex_patterns = [
'deploy','déploie','deploie','migre','migration',
'architecture','design','refonte','reconstru',
'analyse','audit','diagnostic','optimise',
'crée','créer','constru','génère','genere',
'compare','benchmark','stratégie','strategie',
'pourquoi','comment faire','explique en détail',
'code','script','fonction','classe','module',
];
foreach($complex_patterns as $p) if(strpos($lower, $p) !== false) return 'complex';
// Default → medium (Ollama general)
return 'medium';
}
// ═══════════════════════════════════════════════════════
// DIRECT EXECUTION — handles 80% of requests (INSTANT)
// No AI needed — pattern → bash command → formatted response
// ═══════════════════════════════════════════════════════
function execDirect($msg) {
$lower = mb_strtolower(trim($msg));
$cmd = null;
$label = null;
// ── STATUS & HEALTH ──
if(preg_match('/^(status|etat|état|comment.*va|sante|santé|health)/i', $msg)) {
$cmd = 'echo "Disk: $(df -h / | tail -1 | awk \'{print $5}\')" && echo "RAM: $(free -h | awk \'/Mem:/{print $3\"/\"$2}\')" && echo "Load: $(cat /proc/loadavg | cut -d" " -f1-3)" && echo "PHP: $(ps aux | grep php | grep -v grep | wc -l) procs" && echo "PG: $(systemctl is-active postgresql 2>/dev/null)" && echo "Apache: $(systemctl is-active apache2 2>/dev/null)" && echo "Uptime: $(uptime -p)"';
$label = "État S95";
}
// ── ETHICA / HCP / CONTACTS — only data requests, not complex questions ──
elseif(preg_match('/^(ethica|hcp|contact|medecin)$/i', $msg) ||
preg_match('/combien.*contact|stats.*ethica|données.*ethica|data.*ethica|nombre.*hcp/i', $msg)) {
$s204_cmd = 'curl -s --max-time 10 "http://204.168.152.13/api/ethica-api.php?action=dashboard&token=ETHICA_API_2026_SECURE" 2>/dev/null';
$raw = execOnS95($s204_cmd);
$data = json_decode($raw, true);
if($data && isset($data['contacts_total'])) {
$pays = '';
if(is_array($data['contacts_by_pays']??null)) {
foreach($data['contacts_by_pays'] as $p) $pays .= " " . ($p[0]??'') . ": " . number_format($p[1]??0) . "\n";
}
$resp = "**Ethica B2B — Données live**\n\n";
$resp .= "Total HCPs: **" . number_format($data['contacts_total']) . "**\n";
$resp .= "Actifs: " . number_format($data['contacts_active']??0) . "\n";
$resp .= "Senders: " . ($data['senders']??0) . " | Sources: " . ($data['sources']??0) . "\n\n";
if($pays) $resp .= "**Par pays:**\n$pays\n";
$resp .= "Campagnes: " . ($data['campaigns_total']??0) . " | Envois: " . ($data['sends_total']??0);
return ["ok"=>true,"provider"=>"direct","complexity"=>"direct","response"=>$resp,"thinking"=>"Requête API Ethica directe"];
}
}
// ── collecte de donnees ──
elseif(preg_match('/scrap|enrichi/i', $msg)) {
$cmd = 'echo "collecteurs actifs: $(ps aux | grep "ethica.*php" | grep -v grep | wc -l)" && echo "Total HCPs: $(PGPASSWORD=W3v4l_2026_S3cur3 psql -h 127.0.0.1 -U admin -d adx_system -t -c "SELECT COUNT(*) FROM ethica.medecins_real" 2>/dev/null | tr -d " ")" && echo "Sources:" && PGPASSWORD=W3v4l_2026_S3cur3 psql -h 127.0.0.1 -U admin -d adx_system -t -c "SELECT source, COUNT(*) c FROM ethica.medecins_real GROUP BY source ORDER BY c DESC LIMIT 8" 2>/dev/null';
$label = "collecteurs Ethica";
}
// ── INFRA PATTERNS (auto) ──
$infra = matchInfraPattern($msg);
if ($infra && isset($infra['special'])) {
if ($infra['special']==='auth_test_local') {
$results = [];
$ch = curl_init('http://127.0.0.1/api/weval-auth-session.php');
curl_setopt_array($ch, [CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5,CURLOPT_POSTFIELDS=>'action=login&user=weval&pass=YacineWeval2026',CURLOPT_HEADER=>true,CURLOPT_HTTPHEADER=>['Host: weval-consulting.com']]);
$resp = curl_exec($ch); curl_close($ch);
preg_match('/PHPSESSID=([^;\s]+)/', $resp, $m);
$cookie = $m[1] ?? '';
$results[] = 'Session: ' . ($cookie ? 'OK' : 'FAIL');
$pages = ['/wevia-ia/droid.html','/arsenal-proxy/menu.html','/admin-ia','/wevia-bo','/cyber-monitor','/wevia-fullscreen'];
foreach ($pages as $p) {
$ch2 = curl_init('http://127.0.0.1' . $p);
curl_setopt_array($ch2, [CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5,CURLOPT_COOKIE=>'PHPSESSID='.$cookie,CURLOPT_HTTPHEADER=>['Host: weval-consulting.com']]);
$r = curl_exec($ch2); $size = strlen($r); curl_close($ch2);
$results[] = $p . ': ' . ($size > 5000 ? 'OK ' . $size . 'B' : 'FAIL ' . $size . 'B');
}
return ['ok'=>true,'provider'=>'auth-test','complexity'=>'direct','response'=>implode("
",$results)];
}
if ($infra['special']==='heal') { require_once '/var/www/weval/wevia-ia/wevia-self-healing.php'; return ['ok'=>true,'provider'=>'self-heal','complexity'=>'direct','response'=>json_encode(weviaSelfHeal(),JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)]; }
if ($infra['special']==='git') { return ['ok'=>true,'provider'=>'git-auto','complexity'=>'direct','response'=>json_encode(gitAutoPush($msg),JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)]; }
if ($infra['special']==='alert') { return ['ok'=>true,'provider'=>'telegram','complexity'=>'direct','response'=>json_encode(telegramAlert($msg),JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)]; }
if ($infra['special']==='email_check') { $pdo=new PDO('pgsql:host=127.0.0.1;dbname=adx_system','admin',weval_secret('WEVAL_PG_ADMIN_PASS')); $c=$pdo->query("SELECT count(*) as t FROM admin.contact_messages")->fetch(PDO::FETCH_ASSOC); $u=$pdo->query("SELECT count(*) as t FROM admin.users")->fetch(PDO::FETCH_ASSOC); return ['ok'=>true,'provider'=>'email-check','complexity'=>'direct','response'=>"Contacts: ".($c['t']??0)." | Users: ".($u['t']??0)." | Routing: ymahboub+yacineutt"]; }
}
if ($infra && isset($infra['cmd'])) { $cmd = $infra['cmd']; $label = $infra['label']; }
// ── GIT / BACKUP ──
elseif(preg_match('/git|github|commit/i', $msg) && !preg_match('/push|backup|sauvegarde/i', $msg)) {
$cmd = 'cd /opt/wevads-arsenal 2>/dev/null && git log --oneline -5 && echo "---" && git status --short | head -10';
$label = "Git Status";
}
elseif(preg_match('/push|backup|sauvegarde/i', $msg)) {
$cmd = 'cd /opt/wevads-arsenal 2>/dev/null && git add -A && git commit -m "Auto-backup $(date +%Y%m%d-%H%M)" 2>&1 && git push origin master 2>&1 | tail -5 || echo "Rien à committer"';
$label = "Git Push";
}
// ── NONREG / TESTS ──
elseif(preg_match('/nonreg|test|regression|lance.*test|teste/i', $msg)) {
$cmd = 'bash /tmp/nr4.sh 2>&1 | tail -20';
$label = "NonReg Ethica";
}
// ── DISK ──
elseif(preg_match('/disk|disque|espace|storage/i', $msg)) {
$cmd = 'df -h / && echo "---Top dirs:" && du -sh /opt/* 2>/dev/null | sort -rh | head -8';
$label = "Espace disque S95";
}
// ── LOGS ──
elseif(preg_match('/log|error|erreur|debug/i', $msg)) {
$cmd = 'echo "=== Apache ===" && tail -10 /var/log/apache2/error.log 2>/dev/null | tail -5 && echo "=== PHP ===" && tail -5 /var/log/php*.log 2>/dev/null | tail -3';
$label = "Logs récents";
}
// ── CRON ──
elseif(preg_match('/cron|tache|tâche|schedule|planif/i', $msg)) {
$cmd = 'crontab -l 2>/dev/null | grep -v "^#" | grep -v "^$" | head -15';
$label = "Crons actifs";
}
// ── PROCESSES ──
elseif(preg_match('/process|processus|qui tourne|running/i', $msg)) {
$cmd = 'ps aux --sort=-%mem | head -15';
$label = "Top processus";
}
// ── KILL ──
elseif(preg_match('/kill|stop|arrêt|arrete|tue\s/i', $msg)) {
$m = [];
preg_match('/(?:kill|stop|tue)\s+(\S+)/i', $msg, $m);
if(!empty($m[1])) {
$cmd = 'pkill -f "' . $m[1] . '" 2>&1 && echo "' . $m[1] . ' arrêté" || echo "Pas trouvé"';
} else {
return ["ok"=>true,"provider"=>"direct","complexity"=>"direct","response"=>"Dis-moi quoi arrêter : **kill [nom_process]**"];
}
$label = "Kill process";
}
// ── S204 ──
elseif(preg_match('/s204|serveur principal|server principal|nginx/i', $msg)) {
$raw = execOnS95('curl -s --max-time 5 http://204.168.152.13/api/wedroid-brain-api.php?k=DROID2026\\&action=status 2>/dev/null');
$data = json_decode($raw, true);
$resp = "**S204** — OK\n\nProviders IA:\n";
foreach(($data['providers']??[]) as $n=>$p) $resp .= " $n: {$p['status']} ({$p['role']})\n";
return ["ok"=>true,"provider"=>"direct","complexity"=>"direct","response"=>$resp,"thinking"=>"Status S204 via API interne"];
}
// ── S151 ──
elseif(preg_match('/s151|tracking/i', $msg)) {
$raw = execOnS95('curl -s -o /dev/null -w "%{http_code}" --max-time 5 http://151.80.235.110 2>/dev/null');
return ["ok"=>true,"provider"=>"direct","complexity"=>"direct","response"=>"**S151 Tracking**: HTTP $raw"];
}
// ── DIRECT BASH ──
elseif(preg_match('/^[\/$]/', $msg)) {
$cmd = preg_replace('/^[\/$]\s*/', '', $msg);
$label = "Bash";
}
elseif(preg_match('/^(ls|df|ps|top|uptime|whoami|date|pwd|free|cat|head|tail|grep|wc|find|du|ss|curl|systemctl|journalctl|PGPASSWORD)/i', $msg)) {
$cmd = $msg;
$label = "Bash";
}
if($cmd) {
$output = execOnS95($cmd);
$resp = $label ? "**$label:**\n```\n$output\n```" : "```\n$output\n```";
return [
"ok" => true, "provider" => "direct", "complexity" => "direct",
"response" => $resp,
"thinking" => "Exécution directe (pas d'IA nécessaire)",
"commands" => [["cmd" => substr($cmd, 0, 120), "output" => substr($output, 0, 1500), "success" => true]],
];
}
return null;
}
// ═══════════════════════════════════════════════════════
// EXECUTE ON S95 (via SSH from S204)
// ═══════════════════════════════════════════════════════
function execOnS95($cmd) {
global $SSH_CMD;
$escaped = str_replace('"', '\\"', $cmd);
$full = "$SSH_CMD \"$escaped\" 2>&1";
$output = shell_exec($full);
return trim($output ?? "");
}
// ═══════════════════════════════════════════════════════
// CALL OLLAMA (on S95 via SSH)
// ═══════════════════════════════════════════════════════
function callOllama($messages, $model = 'tinyllama:latest') {
global $SSH_CMD;
// Build SHORT prompt for local model (save tokens = faster)
$prompt = "'.WEVAL_BRAND_CONTEXT.'Tu es WEDROID, assistant serveur WEVAL. Réponds en français, court et précis.\n\n";
// Only last 2 messages for local (save tokens)
$recent = array_slice($messages, -2);
foreach($recent as $m) {
$role = $m['role'] === 'user' ? 'Q' : 'R';
$prompt .= "$role: " . substr($m['content'], 0, 500) . "\n";
}
$prompt .= "R:";
$json_payload = json_encode([
"model" => $model,
"prompt" => $prompt,
"stream" => false,
"options" => ["temperature" => 0.2, "num_predict" => 300, "num_ctx" => 2048]
]);
$b64 = base64_encode($json_payload);
// Timeout 30s for tinyllama, 60s for bigger models
$timeout = ($model === 'tinyllama:latest' || $model === 'phi:latest') ? 20 : 45;
$cmd = "echo '$b64' | base64 -d > /tmp/ollama_req.json && curl -s --max-time $timeout http://localhost:11434/api/generate -d @/tmp/ollama_req.json 2>/dev/null";
$raw = execOnS95($cmd);
if(!$raw) return null;
$data = json_decode($raw, true);
return $data['response'] ?? null;
}
// ═══════════════════════════════════════════════════════
// CALL CEREBRAS/GROQ (cloud)
// ═══════════════════════════════════════════════════════
function callCloud($messages, $provider = 'cerebras') {
// === SOVEREIGN FIRST: sovereign-api:4000 has ALL 12 provider keys ===
$sov_ch = curl_init('http://127.0.0.1:4000/v1/chat/completions');
curl_setopt_array($sov_ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'model' => 'auto',
'messages' => $messages,
'max_tokens' => 1500,
'temperature' => 0.3,
])
]);
$sov_resp = curl_exec($sov_ch);
$sov_code = curl_getinfo($sov_ch, CURLINFO_HTTP_CODE);
curl_close($sov_ch);
if ($sov_code === 200) {
$sov_data = json_decode($sov_resp, true);
if (isset($sov_data['choices'][0]['message']['content'])) {
return $sov_data['choices'][0]['message']['content'];
}
}
// === Fallback: direct provider calls (original) ===
global $CEREBRAS_KEY, $CEREBRAS_MODEL, $CEREBRAS_URL, $GROQ_KEY, $GROQ_MODEL, $GROQ_URL;
global $MISTRAL_KEY, $MISTRAL_MODEL, $MISTRAL_URL, $SAMBANOVA_KEY, $SAMBANOVA_MODEL, $SAMBANOVA_URL;
global $DASHSCOPE_KEY, $DASHSCOPE_MODEL, $DASHSCOPE_URL, $OPENROUTER_KEY, $OPENROUTER_MODEL, $OPENROUTER_URL;
$providers = [
'groq' => [$GROQ_URL, $GROQ_KEY, $GROQ_MODEL],
'cerebras' => [$CEREBRAS_URL, $CEREBRAS_KEY, $CEREBRAS_MODEL],
'mistral' => [$MISTRAL_URL, $MISTRAL_KEY, $MISTRAL_MODEL],
'sambanova' => [$SAMBANOVA_URL, $SAMBANOVA_KEY, $SAMBANOVA_MODEL],
'alibaba' => [$DASHSCOPE_URL, $DASHSCOPE_KEY, $DASHSCOPE_MODEL],
'openrouter'=> [$OPENROUTER_URL, $OPENROUTER_KEY, $OPENROUTER_MODEL],
];
if(!isset($providers[$provider])) $provider = 'groq';
[$url, $key, $model] = $providers[$provider];
if(!$key) return null;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $key",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode([
"model" => $model,
"messages" => $messages,
"max_tokens" => 1500,
"temperature" => 0.3,
])
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if($code === 429) return null; // Rate limited → fallback
if($code !== 200) return null;
$data = json_decode($resp, true);
return $data['choices'][0]['message']['content'] ?? null;
}
// ═══════════════════════════════════════════════════════
// AGENTIC LOOP: Think → Plan → Execute → Verify
// ═══════════════════════════════════════════════════════
function agenticProcess($message, $provider_response, $complexity) {
$result = [
"thinking" => null,
"response" => $provider_response,
"commands" => [],
"verify" => null,
"followup" => null,
];
// Extract bash commands from AI response
if(preg_match_all('/```(?:bash|sh)?\s*\n(.*?)\n```/s', $provider_response, $matches)) {
foreach($matches[1] as $cmd_block) {
$cmds = array_filter(explode("\n", trim($cmd_block)));
foreach($cmds as $cmd) {
$cmd = trim($cmd);
if(empty($cmd) || $cmd[0] === '#') continue;
// Safety check
if(preg_match('/rm\s+-rf\s+\/[^t]|mkfs|dd\s+if|shutdown|reboot|poweroff/i', $cmd)) {
$result["commands"][] = ["cmd" => $cmd, "output" => "BLOQUÉ commande dangereuse", "success" => false];
continue;
}
// Execute on S95
$output = execOnS95($cmd);
$result["commands"][] = [
"cmd" => $cmd,
"output" => substr($output, 0, 1000),
"success" => (strlen($output) > 0 || strpos($output, 'error') === false),
"description" => ""
];
}
}
}
// Also handle inline commands like: `commande`
if(empty($result["commands"]) && preg_match_all('/`([^`]+)`/', $provider_response, $inline_matches)) {
foreach($inline_matches[1] as $cmd) {
if(preg_match('/^(curl|ls|cat|grep|ps|df|du|wc|head|tail|echo|systemctl|git|php|bash|PGPASSWORD)/', $cmd)) {
$output = execOnS95($cmd);
$result["commands"][] = ["cmd" => $cmd, "output" => substr($output, 0, 500), "success" => true];
}
}
}
return $result;
}
// ═══════════════════════════════════════════════════════
// MAIN ROUTER
// ═══════════════════════════════════════════════════════
// ═══ EXTENDED ACTIONS (wired by Opus) ═══
if ($action === 'chain') { $r = chainExecute($message); echo json_encode($r); exit; }
if ($action === 'git') { echo json_encode(['ok'=>true,'result'=>gitAutoPush($message?:'auto')]); exit; }
if ($action === 'alert') { echo json_encode(telegramAlert($message, $_POST['level']??'info')); exit; }
if ($action === 'heal') { require_once '/var/www/weval/wevia-ia/wevia-self-healing.php'; echo json_encode(weviaSelfHeal()); exit; }
if ($action === 'schedule') { echo json_encode(wedroidRunSchedule()); exit; }
if ($action === 'learn') { wedroidLearn($message, $_POST['output']??'', true, $_POST['context']??''); echo json_encode(['ok'=>true]); exit; }
if ($action === 'recall') { echo json_encode(['ok'=>true,'knowledge'=>wedroidRecall($message)]); exit; }
if ($action === 'providers') { require_once '/var/www/weval/wevia-ia/wevia-provider-autoscale.php'; echo json_encode(weviaCheckProviders()); exit; }
if ($action === 'rollback') { require_once '/var/www/weval/wevia-ia/wevia-rollback.php'; echo json_encode(weviaListVersions($_POST['path']??'')); exit; }
if ($action === 'email_check') { $pdo=new PDO('pgsql:host=127.0.0.1;dbname=adx_system','admin',weval_secret('WEVAL_PG_ADMIN_PASS')); $c=$pdo->query("SELECT count(*) as t FROM admin.contact_messages")->fetch(PDO::FETCH_ASSOC); $u=$pdo->query("SELECT count(*) as t FROM admin.users")->fetch(PDO::FETCH_ASSOC); echo json_encode(['ok'=>true,'contacts'=>$c['t']??0,'users'=>$u['t']??0,'routing'=>'ymahboub+yacineutt']); exit; }
if($action === 'status') {
echo json_encode([
"ok" => true,
"version" => "5.0",
"providers" => [
"ollama" => ["status" => "active", "models" => array_values($OLLAMA_MODELS), "role" => "primary (80%)"],
"cerebras" => ["status" => $CEREBRAS_KEY ? "active" : "no_key", "model" => $CEREBRAS_MODEL, "role" => "complex (15%)"],
"groq" => ["status" => $GROQ_KEY ? "active" : "no_key", "model" => $GROQ_MODEL, "role" => "fast cloud"],
"mistral" => ["status" => $MISTRAL_KEY ? "active" : "no_key", "model" => $MISTRAL_MODEL, "role" => "multilingual"],
"sambanova" => ["status" => $SAMBANOVA_KEY ? "active" : "no_key", "model" => $SAMBANOVA_MODEL, "role" => "heavy"],
"alibaba" => ["status" => $DASHSCOPE_KEY ? "pending" : "no_key", "model" => $DASHSCOPE_MODEL, "role" => "chinese"],
"openrouter" => ["status" => $OPENROUTER_KEY ? "active" : "no_key", "model" => $OPENROUTER_MODEL, "role" => "free fallback"],
],
"routing" => "SOVEREIGN: ollama>groq>cerebras>mistral>sambanova>alibaba>openrouter",
"session" => $session,
]);
exit;
}
if($action === 'chat' && $message) {
$start = microtime(true);
$complexity = detectComplexity($message);
$provider_used = "unknown";
$response = null;
// STEP 1: Try direct execution (no AI)
$direct = execDirect($message);
if($direct) {
$direct["complexity"] = "direct";
$direct["duration_ms"] = round((microtime(true) - $start) * 1000);
// Save to session
$history[] = ["role" => "user", "content" => $message];
$history[] = ["role" => "assistant", "content" => $direct["response"]];
file_put_contents($session_file, json_encode($history));
echo json_encode($direct);
exit;
}
// STEP 2: Build messages with system prompt + history
$messages = [["role" => "system", "content" => $SYSTEM]];
foreach(array_slice($history, -6) as $h) {
$messages[] = $h;
}
$messages[] = ["role" => "user", "content" => $message];
// STEP 3: Route based on complexity
// STRATEGY: 90% Direct (instant) → 7% Cerebras (1-3s) → 2% Groq → 1% Ollama (emergency)
// LOCAL = Direct bash execution (instant, rich, 90% coverage)
// Ollama on CPU = too slow (17-60s) and low quality — emergency only
// === SOVEREIGN CASCADE: Force provider > Ollama > Cloud chain ===
$cascade = ['groq','cerebras','mistral','sambanova','alibaba','openrouter'];
// Force provider from frontend
if($force_provider && $force_provider !== 'auto') {
if($force_provider === 'ollama' || $force_provider === 'ollama_local') {
$m = ($complexity === 'complex') ? 'qwen3:8b' : 'qwen3:4b';
$response = callCloud($messages, 'cerebras');
$provider_used = 'cerebras-forced';
} else {
$response = callCloud($messages, $force_provider);
$provider_used = $force_provider;
}
}
// Auto routing
if(!$response) {
if($complexity === 'complex') {
// Complex: Cerebras 235B first (best reasoning), then cascade
$cascade = ['cerebras','groq','mistral','sambanova','alibaba','openrouter'];
} elseif($complexity === 'simple') {
// Simple: Ollama local first (sovereign), then fast cloud
$response = callCloud($messages, 'cerebras');
$provider_used = "cerebras";
}
// Medium + fallback: cloud cascade
if(!$response) {
foreach($cascade as $cp) {
$response = callCloud($messages, $cp);
if($response) { $provider_used = $cp; break; }
}
}
// Ultimate fallback: Ollama heavy
if(!$response) {
$response = callCloud($messages, 'groq');
$provider_used = "groq-fallback";
}
}
if(!$response) {
echo json_encode(["ok" => false, "error" => "Tous les providers sont down ou rate-limited"]);
exit;
}
// FLATTEN: if response is nested JSON from Cerebras, extract inner response
if (is_string($response) && trim($response) !== "" && trim($response)[0] === "{") {
$inner = @json_decode(trim($response), true);
if ($inner && isset($inner["response"])) {
$response = $inner["response"];
}
}
// STEP 4: Agentic processing (extract + execute commands)
$result = agenticProcess($message, $response, $complexity);
// STEP 5: Save to session
$history[] = ["role" => "user", "content" => $message];
$history[] = ["role" => "assistant", "content" => $response];
file_put_contents($session_file, json_encode($history));
// STEP 6: Return
echo json_encode([
"ok" => true,
"provider" => $provider_used,
"complexity" => $complexity,
"response" => $result["response"],
"thinking" => $result["thinking"],
"commands" => $result["commands"],
"verify" => $result["verify"],
"followup" => $result["followup"],
"duration_ms" => round((microtime(true) - $start) * 1000),
"session" => $session,
]);
exit;
}
// Default
echo json_encode(["ok" => true, "version" => "5.0", "actions" => ["chat", "status"]]);