331 lines
14 KiB
PHP
Executable File
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;
|
|
}
|
|
}
|