511 lines
24 KiB
PHP
Executable File
511 lines
24 KiB
PHP
Executable File
<?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')
|
||
];
|
||
}
|
||
}
|