Files
wevia-brain/modules/advanced/multi-agent.php
2026-04-12 23:01:36 +02:00

331 lines
14 KiB
PHP
Executable File

<?php
/**
* WEVIA OPUS — Multi-Agent Orchestration
*
* Simule plusieurs experts qui collaborent sur un problème complexe.
* Chaque agent a sa spécialité, son système prompt, et son modèle préféré.
* L'orchestrateur synthétise les contributions.
*
* Pattern: Divide → Delegate → Debate → Synthesize
*/
class MultiAgentOrchestrator {
private string $ollamaUrl;
private array $agents;
private array $conversationLog = [];
public function __construct(string $ollamaUrl = 'http://localhost:11434') {
$this->ollamaUrl = $ollamaUrl;
$this->initializeAgents();
}
private function initializeAgents(): void {
$this->agents = [
'architect' => [
'name' => 'Architecte Solutions',
'model' => 'deepseek-r1:32b',
'specialty' => 'architecture système, scalabilité, patterns de design',
'system_prompt' => "Tu es un architecte solutions senior. Tu penses en termes de:\n"
. "- Scalabilité horizontale vs verticale\n"
. "- Couplage faible, cohésion forte\n"
. "- Patterns: CQRS, Event Sourcing, Saga, Circuit Breaker\n"
. "- Trade-offs: performance vs maintenabilité, coût vs résilience\n"
. "Tu ne proposes JAMAIS de solution sans analyser les trade-offs.\n"
. "Tu dessines mentalement l'architecture avant de répondre.",
'thinking_style' => 'structural',
'priority_topics' => ['architecture', 'scalabilité', 'infrastructure', 'migration', 'design']
],
'security' => [
'name' => 'Expert Sécurité',
'model' => 'qwen2.5-coder:14b',
'specialty' => 'cybersécurité, audit, pentest, conformité',
'system_prompt' => "Tu es un expert cybersécurité (CISSP/OSCP). Tu analyses TOUT sous l'angle sécurité:\n"
. "- Surface d'attaque: chaque endpoint, chaque input, chaque dépendance\n"
. "- OWASP Top 10: injection, auth cassée, XSS, CSRF, SSRF\n"
. "- Principe du moindre privilège\n"
. "- Defense in depth (plusieurs couches)\n"
. "Tu identifies les vulnérabilités AVANT qu'elles soient exploitées.\n"
. "Tu proposes TOUJOURS la remédiation avec le risque.",
'thinking_style' => 'adversarial',
'priority_topics' => ['sécurité', 'audit', 'vulnérabilité', 'authentification', 'chiffrement']
],
'developer' => [
'name' => 'Développeur Senior',
'model' => 'qwen2.5-coder:32b',
'specialty' => 'code, implémentation, debugging, optimisation',
'system_prompt' => "Tu es un développeur fullstack senior (PHP/Python/JS). Tu:\n"
. "- Écris du code propre, testé, documenté\n"
. "- Utilises les design patterns appropriés\n"
. "- Gères les edge cases et erreurs\n"
. "- Optimises les performances (N+1, caching, indexing)\n"
. "- Appliques SOLID, DRY, KISS\n"
. "Tu ne livres JAMAIS du code sans test de validation.",
'thinking_style' => 'implementation',
'priority_topics' => ['code', 'bug', 'implémentation', 'API', 'base de données', 'debug']
],
'data' => [
'name' => 'Data Engineer',
'model' => 'deepseek-r1:14b',
'specialty' => 'données, SQL, ETL, analytics, ML',
'system_prompt' => "Tu es un data engineer senior. Tu penses en termes de:\n"
. "- Modélisation: star schema, snowflake, data vault\n"
. "- Performance SQL: EXPLAIN ANALYZE, index strategy, partitioning\n"
. "- Qualité: validation, déduplication, normalisation\n"
. "- Pipeline: ETL vs ELT, batch vs streaming, idempotence\n"
. "- Scale: quand PostgreSQL suffit vs quand passer à autre chose\n"
. "Tu quantifies TOUT: volumes, latence, coût de stockage.",
'thinking_style' => 'analytical',
'priority_topics' => ['données', 'SQL', 'ETL', 'analytics', 'contacts', 'reporting']
],
'business' => [
'name' => 'Consultant Business',
'model' => 'llama3.3:70b',
'specialty' => 'stratégie, ROI, transformation digitale, marché Maghreb',
'system_prompt' => "Tu es un consultant senior en transformation digitale. Tu:\n"
. "- Traduis la technique en valeur business\n"
. "- Calcules le ROI et le TCO de chaque décision\n"
. "- Connais le marché marocain et maghrébin\n"
. "- Proposes des roadmaps réalistes avec quick wins\n"
. "- Anticipes les risques organisationnels et humains\n"
. "Tu ne proposes JAMAIS de solution sans son business case.",
'thinking_style' => 'strategic',
'priority_topics' => ['stratégie', 'ROI', 'client', 'proposition', 'marché', 'pricing']
],
'email' => [
'name' => 'Expert Email Marketing',
'model' => 'llama3.1:8b',
'specialty' => 'délivrabilité, PowerMTA, warming, ISP, compliance pharma',
'system_prompt' => "Tu es un expert en délivrabilité email B2B pharma. Tu maîtrises:\n"
. "- PowerMTA: VMTA, throttling, DKIM, accounting\n"
. "- ISPs: Gmail (reputation), Outlook (SNDS), Yahoo (CFL)\n"
. "- Warming: schedule progressif, monitoring bounce/complaint\n"
. "- SPF/DKIM/DMARC: configuration, alignment, diagnostic\n"
. "- Compliance: Loi 09-08, RGPD, réglementation pharma Maghreb\n"
. "Tu diagnostiques les problèmes de délivrabilité en 3 étapes max.",
'thinking_style' => 'diagnostic',
'priority_topics' => ['email', 'délivrabilité', 'spam', 'bounce', 'warmup', 'PowerMTA', 'DKIM']
]
];
}
/**
* Route une question vers le(s) agent(s) le(s) plus pertinent(s)
*/
public function route(string $query): array {
$queryLower = mb_strtolower($query);
$scores = [];
foreach ($this->agents as $id => $agent) {
$score = 0;
foreach ($agent['priority_topics'] as $topic) {
if (str_contains($queryLower, $topic)) {
$score += 10;
}
}
// Bonus si la spécialité matche
$specialtyWords = explode(', ', $agent['specialty']);
foreach ($specialtyWords as $word) {
if (str_contains($queryLower, trim($word))) {
$score += 5;
}
}
$scores[$id] = $score;
}
arsort($scores);
// Sélectionner les agents pertinents (score > 0, max 3)
$selected = [];
foreach ($scores as $id => $score) {
if ($score > 0 && count($selected) < 3) {
$selected[] = $id;
}
}
// Si aucun agent spécifique, utiliser architect + developer (défaut)
if (empty($selected)) {
$selected = ['architect', 'developer'];
}
return $selected;
}
/**
* Consulte un agent spécifique
*/
public function consultAgent(string $agentId, string $query, array $context = []): array {
$agent = $this->agents[$agentId] ?? null;
if (!$agent) return ['error' => "Agent '$agentId' not found"];
$messages = [
['role' => 'system', 'content' => $agent['system_prompt']],
];
// Ajouter le contexte des autres agents si disponible
if (!empty($context)) {
$contextStr = "Contexte des autres experts:\n";
foreach ($context as $ctx) {
$contextStr .= "- {$ctx['agent']}: {$ctx['summary']}\n";
}
$messages[] = ['role' => 'user', 'content' => $contextStr . "\nQuestion: " . $query];
} else {
$messages[] = ['role' => 'user', 'content' => $query];
}
$startTime = microtime(true);
$response = $this->callOllama($agent['model'], $messages);
$duration = round((microtime(true) - $startTime) * 1000);
$result = [
'agent_id' => $agentId,
'agent_name' => $agent['name'],
'model' => $agent['model'],
'thinking_style' => $agent['thinking_style'],
'response' => $response,
'duration_ms' => $duration
];
$this->conversationLog[] = $result;
return $result;
}
/**
* Processus complet: Route → Consult → Debate → Synthesize
*/
public function solve(string $query): array {
$startTime = microtime(true);
// 1. Route vers les bons agents
$selectedAgents = $this->route($query);
// 2. Phase 1: Consultation parallèle (chaque agent répond indépendamment)
$phase1 = [];
foreach ($selectedAgents as $agentId) {
$phase1[$agentId] = $this->consultAgent($agentId, $query);
}
// 3. Phase 2: Debate (chaque agent voit les réponses des autres)
$context = [];
foreach ($phase1 as $agentId => $result) {
$context[] = [
'agent' => $result['agent_name'],
'summary' => $this->summarize($result['response'], 200)
];
}
$phase2 = [];
foreach ($selectedAgents as $agentId) {
$debateQuery = "En tenant compte des perspectives des autres experts, "
. "affine ta réponse. Identifie les points d'accord et de désaccord. "
. "Question originale: " . $query;
$phase2[$agentId] = $this->consultAgent($agentId, $debateQuery, $context);
}
// 4. Synthèse finale
$synthesis = $this->synthesize($query, $phase1, $phase2);
$totalDuration = round((microtime(true) - $startTime) * 1000);
return [
'query' => $query,
'agents_consulted' => $selectedAgents,
'phase1_individual' => $phase1,
'phase2_debate' => $phase2,
'synthesis' => $synthesis,
'total_duration_ms' => $totalDuration,
'conversation_log' => $this->conversationLog
];
}
/**
* Synthétise les contributions de tous les agents
*/
private function synthesize(string $query, array $phase1, array $phase2): string {
$synthesisPrompt = "Tu es le synthétiseur WEVIA. Voici les contributions de plusieurs experts.\n\n";
$synthesisPrompt .= "Question: $query\n\n";
foreach ($phase2 as $agentId => $result) {
$agentName = $result['agent_name'];
$response = $this->summarize($result['response'], 500);
$synthesisPrompt .= "### $agentName\n$response\n\n";
}
$synthesisPrompt .= "Synthétise une réponse complète et cohérente:\n"
. "1. Commence par la recommandation principale\n"
. "2. Intègre les perspectives complémentaires\n"
. "3. Mentionne les risques identifiés\n"
. "4. Propose un plan d'action concret\n"
. "5. Si les experts étaient en désaccord, explique pourquoi et tranche";
$messages = [
['role' => 'system', 'content' => 'Tu es WEVIA, synthétiseur expert. Tu combines les avis de plusieurs spécialistes en une réponse cohérente, actionable, et complète.'],
['role' => 'user', 'content' => $synthesisPrompt]
];
return $this->callOllama('deepseek-r1:32b', $messages);
}
/**
* Résume un texte à N mots max
*/
private function summarize(string $text, int $maxWords): string {
$words = explode(' ', $text);
if (count($words) <= $maxWords) return $text;
return implode(' ', array_slice($words, 0, $maxWords)) . '...';
}
/**
* Appel Ollama API
*/
private function callOllama(string $model, array $messages): string {
$payload = json_encode([
'model' => $model,
'messages' => $messages,
'stream' => false,
'options' => [
'temperature' => 0.7,
'num_predict' => 2048,
'top_p' => 0.9
]
]);
$ch = curl_init("{$this->ollamaUrl}/api/chat");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 120
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) return "[ERROR] Ollama returned HTTP $httpCode";
$data = json_decode($response, true);
return $data['message']['content'] ?? '[ERROR] No content in response';
}
/**
* Liste les agents disponibles
*/
public function getAgents(): array {
$list = [];
foreach ($this->agents as $id => $agent) {
$list[$id] = [
'name' => $agent['name'],
'model' => $agent['model'],
'specialty' => $agent['specialty'],
'thinking_style' => $agent['thinking_style']
];
}
return $list;
}
}