Files
wevia-brain/modules/core/metacognition.php
2026-04-12 23:01:36 +02:00

511 lines
24 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
/**
* WEVIA OPUS — Metacognition Engine
*
* Ce module implémente la réflexion de second ordre:
* WEVIA ne pense pas seulement au problème, elle pense à
* COMMENT elle pense au problème. C'est ce qui différencie
* Opus d'un simple LLM.
*
* Capacités:
* - Calibration de confiance (sait quand elle ne sait pas)
* - Détection de biais dans son propre raisonnement
* - Sélection dynamique de stratégie cognitive
* - Auto-évaluation de la qualité de réponse
* - Détection d'hallucination pré-réponse
*/
class MetacognitionEngine {
private string $ollamaUrl;
private array $cognitiveStrategies;
private array $biasPatterns;
private array $confidenceHistory = [];
public function __construct(string $ollamaUrl = 'http://localhost:11434') {
$this->ollamaUrl = $ollamaUrl;
$this->initStrategies();
$this->initBiasPatterns();
}
private function initStrategies(): void {
$this->cognitiveStrategies = [
'analytical' => [
'name' => 'Analyse décomposée',
'when' => 'Problèmes techniques, debugging, architecture',
'steps' => ['décomposer', 'analyser chaque partie', 'synthétiser', 'vérifier'],
'models' => ['deepseek-r1:32b', 'qwen2.5-coder:32b'],
'temperature' => 0.2,
'strengths' => 'Précision, rigueur logique, couverture exhaustive',
'weaknesses' => 'Peut sur-analyser des problèmes simples, lent'
],
'creative' => [
'name' => 'Pensée divergente',
'when' => 'Brainstorming, solutions innovantes, naming, design',
'steps' => ['générer largement', 'associer librement', 'filtrer', 'affiner'],
'models' => ['llama3.3:70b', 'mixtral:8x22b'],
'temperature' => 0.9,
'strengths' => 'Originalité, associations inattendues',
'weaknesses' => 'Peut manquer de rigueur, idées non réalisables'
],
'systematic' => [
'name' => 'Approche systématique',
'when' => 'Process, checklist, audit, migration, déploiement',
'steps' => ['lister toutes les étapes', 'ordonner', 'identifier dépendances', 'exécuter séquentiellement'],
'models' => ['deepseek-r1:32b', 'nemotron:70b'],
'temperature' => 0.1,
'strengths' => 'Complétude, rien oublié, reproductible',
'weaknesses' => 'Rigide, peut manquer de créativité'
],
'analogical' => [
'name' => 'Raisonnement par analogie',
'when' => 'Explication de concepts, formation, vulgarisation',
'steps' => ['identifier le concept cible', 'trouver un domaine familier', 'mapper les relations', 'valider les limites de l\'analogie'],
'models' => ['llama3.3:70b', 'llama3.1:70b'],
'temperature' => 0.6,
'strengths' => 'Accessibilité, mémorabilité, engagement',
'weaknesses' => 'Analogie peut être trompeuse si poussée trop loin'
],
'adversarial' => [
'name' => 'Pensée adversariale',
'when' => 'Sécurité, test de robustesse, évaluation de risques',
'steps' => ['identifier la surface d\'attaque', 'penser comme un attaquant', 'trouver les failles', 'proposer les défenses'],
'models' => ['deepseek-r1:32b', 'qwen2.5-coder:14b'],
'temperature' => 0.3,
'strengths' => 'Trouve les failles que l\'approche constructive manque',
'weaknesses' => 'Peut être trop pessimiste, voir des risques partout'
],
'empirical' => [
'name' => 'Approche empirique',
'when' => 'Questions avec données, analytics, benchmarks',
'steps' => ['collecter les données', 'analyser statistiquement', 'identifier les patterns', 'conclure avec intervalles de confiance'],
'models' => ['deepseek-r1:14b', 'codestral:22b'],
'temperature' => 0.2,
'strengths' => 'Basé sur les faits, quantifié, vérifiable',
'weaknesses' => 'Besoin de données, corrélation ≠ causalité'
],
'first_principles' => [
'name' => 'Raisonnement par premiers principes',
'when' => 'Innovation, remise en question d\'hypothèses, optimisation radicale',
'steps' => ['identifier les hypothèses implicites', 'remonter aux vérités fondamentales', 'reconstruire depuis zéro', 'comparer avec l\'approche classique'],
'models' => ['deepseek-r1:32b', 'nemotron:70b'],
'temperature' => 0.4,
'strengths' => 'Solutions non-conventionnelles, optimisation maximale',
'weaknesses' => 'Coûteux en temps de réflexion, peut réinventer la roue'
]
];
}
private function initBiasPatterns(): void {
$this->biasPatterns = [
'anchoring' => [
'description' => 'Se fixer sur la première information reçue',
'detection' => 'La réponse est-elle trop influencée par le premier fait mentionné?',
'mitigation' => 'Considérer d\'autres perspectives avant de conclure'
],
'confirmation' => [
'description' => 'Chercher des preuves qui confirment ce qu\'on croit déjà',
'detection' => 'Ai-je cherché activement des contre-exemples?',
'mitigation' => 'Lister 3 raisons pour lesquelles ma réponse pourrait être fausse'
],
'availability' => [
'description' => 'Surestimer ce qui vient facilement à l\'esprit',
'detection' => 'Ma réponse est-elle basée sur des exemples récents plutôt que représentatifs?',
'mitigation' => 'Chercher des données systématiques, pas anecdotiques'
],
'authority' => [
'description' => 'Accepter sans critique les sources faisant autorité',
'detection' => 'Ai-je vérifié les claims même des sources "fiables"?',
'mitigation' => 'Vérifier la source primaire, pas juste la citation'
],
'sunk_cost' => [
'description' => 'Continuer parce qu\'on a déjà investi, pas parce que c\'est la bonne direction',
'detection' => 'Si je repartais de zéro, ferais-je le même choix?',
'mitigation' => 'Évaluer l\'option actuelle comme si c\'était une nouvelle proposition'
],
'recency' => [
'description' => 'Favoriser les informations récentes sur les anciennes',
'detection' => 'Les informations les plus récentes sont-elles vraiment les plus pertinentes?',
'mitigation' => 'Considérer la tendance à long terme, pas juste le dernier data point'
],
'dunning_kruger' => [
'description' => 'Surestimer sa compétence dans un domaine peu maîtrisé',
'detection' => 'Est-ce un domaine où j\'ai une expertise vérifiable?',
'mitigation' => 'Calibrer sa confiance: dire "je ne suis pas expert en..." quand c\'est le cas'
],
'framing' => [
'description' => 'La réponse change selon comment la question est formulée',
'detection' => 'Reformuler la question autrement change-t-il ma réponse?',
'mitigation' => 'Reformuler la question de 2-3 façons différentes et vérifier la cohérence'
]
];
}
/**
* Sélectionne la meilleure stratégie cognitive pour une question
*/
public function selectStrategy(string $query, array $context = []): array {
$queryLower = mb_strtolower($query);
$scores = [];
// Scoring par mots-clés et patterns
$strategySignals = [
'analytical' => ['debug', 'erreur', 'bug', 'pourquoi', 'cause', 'problème', 'analyse', 'comment fonctionne', 'architecture', 'technique'],
'creative' => ['idée', 'brainstorm', 'nom', 'design', 'innov', 'créati', 'imagin', 'altern', 'suggestion'],
'systematic' => ['étape', 'process', 'checklist', 'migration', 'audit', 'plan', 'déploie', 'procédure', 'workflow'],
'analogical' => ['explique', 'c\'est quoi', 'définit', 'compare', 'comme', 'différence entre', 'vulgarise'],
'adversarial' => ['sécuri', 'vulnérab', 'risque', 'attaque', 'faille', 'pentest', 'audit sécu', 'danger'],
'empirical' => ['donnée', 'stat', 'métrique', 'KPI', 'benchmark', 'mesure', 'combien', 'performance', 'chiffre'],
'first_principles' => ['fondamental', 'optimis', 'radical', 'repens', 'zero', 'pourquoi on fait', 'meilleur moyen']
];
foreach ($strategySignals as $strategy => $signals) {
$score = 0;
foreach ($signals as $signal) {
if (mb_stripos($queryLower, $signal) !== false) {
$score += 3;
}
}
$scores[$strategy] = $score;
}
// Si question complexe (longue, multi-phrases) → analytical
if (mb_strlen($query) > 200) {
$scores['analytical'] += 2;
}
// Si contient du code → analytical
if (preg_match('/```|function |class |SELECT |def |import /', $query)) {
$scores['analytical'] += 5;
}
// Si question courte et simple → analogical (explication)
if (mb_strlen($query) < 50 && preg_match('/\?$/', $query)) {
$scores['analogical'] += 2;
}
arsort($scores);
$topStrategy = array_key_first($scores);
// Si aucun signal clair, défaut à analytical
if ($scores[$topStrategy] === 0) {
$topStrategy = 'analytical';
}
$strategy = $this->cognitiveStrategies[$topStrategy];
return [
'selected' => $topStrategy,
'name' => $strategy['name'],
'steps' => $strategy['steps'],
'model' => $strategy['models'][0],
'temperature' => $strategy['temperature'],
'confidence' => min(1.0, $scores[$topStrategy] / 10),
'alternatives' => array_slice(array_keys($scores), 1, 2),
'reasoning' => "Stratégie '{$strategy['name']}' sélectionnée car: {$strategy['when']}"
];
}
/**
* Calibre la confiance de WEVIA dans sa réponse
*/
public function calibrateConfidence(string $query, string $response, string $domain): array {
$confidence = 0.7; // Base
$factors = [];
// Domaines de haute expertise WEVAL → haute confiance
$highExpertise = ['email', 'powermta', 'deliverability', 'sap', 'wevads', 'arsenal', 'postgresql', 'php', 'linux', 'dns', 'dkim', 'spf', 'dmarc'];
$medExpertise = ['python', 'javascript', 'docker', 'kubernetes', 'aws', 'azure', 'react', 'machine learning'];
$lowExpertise = ['médecine', 'droit', 'finance', 'comptabilité', 'biologie'];
$domainLower = mb_strtolower($domain);
foreach ($highExpertise as $d) {
if (mb_stripos($domainLower, $d) !== false) {
$confidence += 0.2;
$factors[] = "Domaine d'expertise WEVAL: +0.2";
break;
}
}
foreach ($lowExpertise as $d) {
if (mb_stripos($domainLower, $d) !== false) {
$confidence -= 0.3;
$factors[] = "Domaine hors expertise: -0.3";
break;
}
}
// Réponse contient des caveats → calibration honnête
$hedgeWords = ['probablement', 'il est possible', 'je ne suis pas sûr', 'à vérifier', 'environ', 'roughly'];
foreach ($hedgeWords as $hw) {
if (mb_stripos($response, $hw) !== false) {
$confidence -= 0.05;
$factors[] = "Hedging détecté: -0.05";
}
}
// Réponse contient des chiffres/stats → vérifier si sourcés
if (preg_match('/\d+%|\$\d+|\d+ (millions?|milliards?)/', $response)) {
if (!preg_match('/selon|d\'après|source|étude|rapport/', $response)) {
$confidence -= 0.15;
$factors[] = "Chiffres non sourcés: -0.15";
}
}
// Longueur de la réponse vs complexité de la question
$queryComplexity = mb_strlen($query) / 50; // Simple proxy
$responseLength = mb_strlen($response);
if ($queryComplexity > 3 && $responseLength < 200) {
$confidence -= 0.1;
$factors[] = "Question complexe, réponse courte: -0.1";
}
$confidence = max(0.1, min(1.0, $confidence));
// Classification
$level = 'high';
if ($confidence < 0.5) $level = 'low';
elseif ($confidence < 0.75) $level = 'medium';
// Recommandation
$recommendation = '';
if ($level === 'low') {
$recommendation = "⚠️ Confiance faible. Recommandation: caveater la réponse, suggérer de vérifier, ou demander plus de contexte.";
} elseif ($level === 'medium') {
$recommendation = " Confiance moyenne. Recommandation: mentionner les incertitudes, proposer des alternatives.";
}
$this->confidenceHistory[] = ['confidence' => $confidence, 'domain' => $domain, 'timestamp' => time()];
return [
'confidence' => round($confidence, 2),
'level' => $level,
'factors' => $factors,
'recommendation' => $recommendation,
'domain' => $domain
];
}
/**
* Détecte les biais potentiels dans un raisonnement
*/
public function detectBias(string $query, string $response): array {
$detectedBiases = [];
// Anchoring: la réponse reprend le premier élément de la question
$firstSentence = strtok($query, '.!?');
$responseStart = mb_substr($response, 0, 200);
if ($firstSentence && similar_text(mb_strtolower($firstSentence), mb_strtolower($responseStart)) > mb_strlen($firstSentence) * 0.5) {
$detectedBiases[] = [
'bias' => 'anchoring',
'description' => $this->biasPatterns['anchoring']['description'],
'mitigation' => $this->biasPatterns['anchoring']['mitigation'],
'severity' => 'medium'
];
}
// Confirmation: pas de contre-arguments
$counterIndicators = ['cependant', 'toutefois', 'néanmoins', 'en revanche', 'mais', 'however', 'although', 'inconvénient', 'risque', 'limite'];
$hasCounter = false;
foreach ($counterIndicators as $ci) {
if (mb_stripos($response, $ci) !== false) {
$hasCounter = true;
break;
}
}
if (!$hasCounter && mb_strlen($response) > 300) {
$detectedBiases[] = [
'bias' => 'confirmation',
'description' => $this->biasPatterns['confirmation']['description'],
'mitigation' => $this->biasPatterns['confirmation']['mitigation'],
'severity' => 'high'
];
}
// Authority: cite beaucoup de noms sans vérification
if (preg_match_all('/selon (Google|Microsoft|SAP|Gartner|McKinsey|Forrester)/i', $response, $authMatches)) {
if (count($authMatches[0]) > 2) {
$detectedBiases[] = [
'bias' => 'authority',
'description' => $this->biasPatterns['authority']['description'],
'mitigation' => $this->biasPatterns['authority']['mitigation'],
'severity' => 'low'
];
}
}
// Dunning-Kruger: réponse très confiante sur domaine hors expertise
$uncertainDomains = ['juridique', 'médical', 'fiscal', 'comptable', 'réglementaire'];
$isUncertainDomain = false;
foreach ($uncertainDomains as $ud) {
if (mb_stripos($query, $ud) !== false) {
$isUncertainDomain = true;
break;
}
}
if ($isUncertainDomain && !preg_match('/je ne suis pas (expert|spécialiste|certain)|à vérifier|consultez un/i', $response)) {
$detectedBiases[] = [
'bias' => 'dunning_kruger',
'description' => $this->biasPatterns['dunning_kruger']['description'],
'mitigation' => $this->biasPatterns['dunning_kruger']['mitigation'],
'severity' => 'high'
];
}
return [
'biases_detected' => count($detectedBiases),
'biases' => $detectedBiases,
'overall_risk' => count($detectedBiases) === 0 ? 'low' : (count($detectedBiases) > 2 ? 'high' : 'medium')
];
}
/**
* Pré-vérifie une réponse pour les hallucinations potentielles
*/
public function hallucination_check(string $response): array {
$risks = [];
// URLs inventées
if (preg_match_all('/https?:\/\/[^\s<>"]+/', $response, $urls)) {
foreach ($urls[0] as $url) {
// Vérifier si c'est un domaine connu
$knownDomains = ['github.com', 'stackoverflow.com', 'docs.python.org', 'php.net',
'developer.mozilla.org', 'postgresql.org', 'sap.com', 'microsoft.com',
'google.com', 'weval-consulting.com', 'claude.ai', 'anthropic.com'];
$domain = parse_url($url, PHP_URL_HOST);
$isKnown = false;
foreach ($knownDomains as $kd) {
if (str_contains($domain ?? '', $kd)) { $isKnown = true; break; }
}
if (!$isKnown) {
$risks[] = ['type' => 'unknown_url', 'value' => $url, 'risk' => 'medium',
'note' => "URL potentiellement inventée — vérifier l'existence"];
}
}
}
// Statistiques trop précises sans source
if (preg_match_all('/(\d{2,}\.?\d*)\s*%/', $response, $percentages)) {
foreach ($percentages[0] as $pct) {
$num = (float)$pct;
if ($num > 0 && $num != 100 && $num != 50 && $num != 25 && $num != 75) {
// Nombre très précis, potentiellement inventé
if (!preg_match('/selon|source|étude|rapport|benchmark/i', $response)) {
$risks[] = ['type' => 'unsourced_stat', 'value' => $pct,
'risk' => 'medium', 'note' => 'Statistique précise sans source'];
}
}
}
}
// Noms de produits/features potentiellement inventés
$suspiciousPatterns = [
'/SAP\s+[A-Z][a-z]+\s+[A-Z][a-z]+/' => 'Nom de produit SAP potentiellement inventé',
'/version\s+\d+\.\d+\.\d+/' => 'Numéro de version très spécifique — vérifier',
];
foreach ($suspiciousPatterns as $pattern => $note) {
if (preg_match($pattern, $response, $m)) {
$risks[] = ['type' => 'suspicious_name', 'value' => $m[0], 'risk' => 'low', 'note' => $note];
}
}
// Citations de personnes
if (preg_match('/selon\s+(\w+\s+\w+),|comme\s+l\'a\s+dit\s+(\w+\s+\w+)/i', $response, $quotes)) {
$person = $quotes[1] ?: $quotes[2];
$risks[] = ['type' => 'person_quote', 'value' => $person, 'risk' => 'high',
'note' => 'Citation attribuée à une personne — vérifier absolument'];
}
$riskLevel = 'low';
$highRisks = count(array_filter($risks, fn($r) => $r['risk'] === 'high'));
if ($highRisks > 0) $riskLevel = 'high';
elseif (count($risks) > 2) $riskLevel = 'medium';
return [
'hallucination_risk' => $riskLevel,
'risk_count' => count($risks),
'risks' => $risks,
'recommendation' => $riskLevel === 'high' ?
'ATTENTION: Risque élevé d\'hallucination. Vérifier les faits avant de livrer.' :
($riskLevel === 'medium' ? 'Quelques éléments à vérifier.' : 'Risque faible.')
];
}
/**
* Auto-évalue la qualité globale d'une réponse
*/
public function evaluateResponse(string $query, string $response): array {
$scores = [];
// 1. Pertinence (la réponse adresse-t-elle la question?)
$queryKeywords = $this->extractKeywords($query);
$matchedKeywords = 0;
foreach ($queryKeywords as $kw) {
if (mb_stripos($response, $kw) !== false) $matchedKeywords++;
}
$scores['relevance'] = count($queryKeywords) > 0 ?
min(10, round(($matchedKeywords / count($queryKeywords)) * 10, 1)) : 7;
// 2. Complétude
$questionMarks = substr_count($query, '?');
$sections = preg_match_all('/#{1,3}\s|^\d+\.\s|\*\*.*\*\*/m', $response);
$scores['completeness'] = min(10, 5 + $sections + ($questionMarks > 1 ? 2 : 0));
// 3. Actionabilité (contient des actions concrètes?)
$actionIndicators = ['```', 'commande', 'étape', 'faire', 'configurer', 'installer', 'créer', 'modifier', 'exécuter'];
$actionScore = 0;
foreach ($actionIndicators as $ai) {
if (mb_stripos($response, $ai) !== false) $actionScore++;
}
$scores['actionability'] = min(10, $actionScore * 2 + 2);
// 4. Clarté
$avgSentenceLength = mb_strlen($response) / max(1, substr_count($response, '.'));
$scores['clarity'] = $avgSentenceLength < 150 ? 8 : ($avgSentenceLength < 250 ? 6 : 4);
// 5. Sécurité
$hallCheck = $this->hallucination_check($response);
$scores['safety'] = $hallCheck['hallucination_risk'] === 'low' ? 10 :
($hallCheck['hallucination_risk'] === 'medium' ? 6 : 3);
// Score global pondéré
$weights = ['relevance' => 0.3, 'completeness' => 0.2, 'actionability' => 0.2, 'clarity' => 0.15, 'safety' => 0.15];
$globalScore = 0;
foreach ($weights as $dim => $weight) {
$globalScore += ($scores[$dim] ?? 5) * $weight;
}
return [
'scores' => $scores,
'global_score' => round($globalScore, 1),
'grade' => $globalScore >= 8 ? 'A' : ($globalScore >= 6 ? 'B' : ($globalScore >= 4 ? 'C' : 'D')),
'should_revise' => $globalScore < 6,
'weakest_dimension' => array_search(min($scores), $scores)
];
}
private function extractKeywords(string $text): array {
$stopWords = ['le','la','les','de','du','des','un','une','et','ou','est','que','qui','dans',
'pour','par','sur','avec','ce','cette','mon','ma','mes','comment','quoi','quel',
'quand','je','tu','il','nous','vous','ils','ne','pas','plus','the','a','is','in',
'on','at','to','for','of','with','what','how','why','where','when'];
$words = preg_split('/[\s,.;:!?\'\"()\[\]{}]+/', mb_strtolower($text));
return array_values(array_filter($words, fn($w) => mb_strlen($w) > 2 && !in_array($w, $stopWords)));
}
/**
* Pipeline complet de métacognition
*/
public function process(string $query, string $response, string $domain = 'general'): array {
return [
'strategy' => $this->selectStrategy($query),
'confidence' => $this->calibrateConfidence($query, $response, $domain),
'bias_check' => $this->detectBias($query, $response),
'hallucination_check' => $this->hallucination_check($response),
'quality_eval' => $this->evaluateResponse($query, $response),
'timestamp' => date('Y-m-d H:i:s')
];
}
}