auto-sync via WEVIA git_sync_all intent 2026-04-21T02:48:20+02:00
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
This commit is contained in:
BIN
api/playwright-v100/01-wtp-home-v85-card.png
Normal file
BIN
api/playwright-v100/01-wtp-home-v85-card.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 660 KiB |
40
api/playwright-v100/results.json
Normal file
40
api/playwright-v100/results.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"tests": [
|
||||
{
|
||||
"test": "v83_has_architecture_quality",
|
||||
"status": "PASS",
|
||||
"cats": 8,
|
||||
"kpis": 8
|
||||
},
|
||||
{
|
||||
"test": "v83_summary_updated",
|
||||
"status": "PASS",
|
||||
"total_categories": 8,
|
||||
"total_kpis": 64,
|
||||
"ok": 37,
|
||||
"warn": 27,
|
||||
"fail": 0,
|
||||
"wire_needed": 0,
|
||||
"data_completeness_pct": 100
|
||||
},
|
||||
{
|
||||
"test": "wtp_v85_reflects_v100",
|
||||
"status": "PASS",
|
||||
"total": "64",
|
||||
"cats": "8",
|
||||
"live": "37",
|
||||
"complete": "100%"
|
||||
},
|
||||
{
|
||||
"test": "wtp_v85_shows_architecture_quality",
|
||||
"status": "PASS",
|
||||
"grid_len": 483
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"pass": 4,
|
||||
"fail": 0,
|
||||
"total": 4
|
||||
},
|
||||
"ts": "2026-04-21T00:47:35.046Z"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"ok": true,
|
||||
"version": "V83-business-kpi",
|
||||
"ts": "2026-04-21T00:45:54+00:00",
|
||||
"ts": "2026-04-21T00:48:03+00:00",
|
||||
"summary": {
|
||||
"total_categories": 8,
|
||||
"total_kpis": 64,
|
||||
|
||||
913
api/wevia-claude-code-patterns.php
Normal file
913
api/wevia-claude-code-patterns.php
Normal file
@@ -0,0 +1,913 @@
|
||||
<?php
|
||||
/**
|
||||
* WEVIA Claude Code Patterns v1.0 — 31 mars 2026
|
||||
*
|
||||
* Architectural patterns extracted from Claude Code source leak analysis:
|
||||
* 1. Token Budget Continuation — auto-resume on truncated outputs
|
||||
* 2. Three-Layer Context Compression — snip/compact/collapse
|
||||
* 3. Provider Effort Levels — intent complexity → provider tier mapping
|
||||
* 4. Deferred Tool Discovery — core vs specialized tool split (-85% tokens)
|
||||
* 5. Tool Alphabetical Sorting — prompt cache stability
|
||||
* 6. Memory Consolidation Framework — Dream-like session memory
|
||||
* 7. Enhanced Circuit Breaker — per-provider health tracking with decay
|
||||
* 8. Concurrency Safety Tiers — read-only vs write tool classification
|
||||
*
|
||||
* Usage: require_once __DIR__ . '/wevia-claude-code-patterns.php';
|
||||
* All functions are prefixed with wcp_ (wevia-claude-patterns)
|
||||
*/
|
||||
|
||||
define('WCP_VERSION', '1.0.0');
|
||||
define('WCP_MAX_MEMORY_LINES', 200); // Dream consolidation cap (from Claude Code)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 1. TOKEN BUDGET CONTINUATION
|
||||
// When a provider truncates output mid-task, inject invisible
|
||||
// meta-message and retry. Up to 3 recovery attempts.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wcp_detect_truncation($response, $finish_reason = null) {
|
||||
// Explicit truncation signal from API
|
||||
if ($finish_reason === 'length') return true;
|
||||
|
||||
// Heuristic: response ends mid-sentence (no terminal punctuation)
|
||||
$trimmed = rtrim($response);
|
||||
if (empty($trimmed)) return false;
|
||||
|
||||
$lastChar = mb_substr($trimmed, -1);
|
||||
$terminals = ['.', '!', '?', ':', ';', '```', '>', ')', ']', '}', '"', "'", "\n"];
|
||||
|
||||
// If response is long enough and doesn't end with terminal punctuation
|
||||
if (mb_strlen($trimmed) > 200 && !in_array($lastChar, $terminals)) {
|
||||
// Additional check: ends mid-code-block?
|
||||
$openBlocks = substr_count($trimmed, '```');
|
||||
if ($openBlocks % 2 !== 0) return true; // Unclosed code block
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function wcp_continuation_message($partial_response) {
|
||||
// Invisible meta-message that instructs the model to resume
|
||||
// Pattern from Claude Code: "Resume directly — no apology, no recap"
|
||||
return [
|
||||
"role" => "user",
|
||||
"content" => "[SYSTEM: Your previous response was truncated at the token limit. Continue EXACTLY where you left off. Do NOT apologize, do NOT recap what you already said, do NOT restart. Resume mid-sentence if needed. Here is your last 200 chars for context: \"" . mb_substr($partial_response, -200) . "\"]"
|
||||
];
|
||||
}
|
||||
|
||||
function wcp_auto_continue($callProvider, $messages, $sys, $provider, $maxAttempts = 3) {
|
||||
$fullResponse = '';
|
||||
$attempts = 0;
|
||||
|
||||
while ($attempts < $maxAttempts) {
|
||||
$result = $callProvider($messages, $sys, $provider);
|
||||
if (!$result || empty($result['response'])) break;
|
||||
|
||||
$fullResponse .= $result['response'];
|
||||
$attempts++;
|
||||
|
||||
// Check if truncated
|
||||
$finishReason = $result['finish_reason'] ?? null;
|
||||
if (!wcp_detect_truncation($result['response'], $finishReason)) {
|
||||
break; // Complete response
|
||||
}
|
||||
|
||||
// Inject continuation message
|
||||
$messages[] = ["role" => "assistant", "content" => $result['response']];
|
||||
$messages[] = wcp_continuation_message($result['response']);
|
||||
|
||||
error_log("WCP_CONTINUE: attempt=$attempts provider=$provider partial_len=" . mb_strlen($result['response']));
|
||||
}
|
||||
|
||||
return [
|
||||
'response' => $fullResponse,
|
||||
'attempts' => $attempts,
|
||||
'continued' => $attempts > 1
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 2. THREE-LAYER CONTEXT COMPRESSION
|
||||
// Layer 1: snipCompact — trim low-value segments
|
||||
// Layer 2: autoCompact — summarize conversation history
|
||||
// Layer 3: contextCollapse — restructure for provider limits
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wcp_snip_compact($history, $maxTokenEstimate = 8000) {
|
||||
/**
|
||||
* Layer 1: Remove low-value exchanges from history
|
||||
* - Greetings and acknowledgments
|
||||
* - Repeated questions
|
||||
* - Error messages that were resolved
|
||||
*/
|
||||
if (empty($history)) return $history;
|
||||
|
||||
$snipped = [];
|
||||
$totalEstTokens = 0;
|
||||
|
||||
// Process from newest to oldest (keep recent context)
|
||||
$reversed = array_reverse($history);
|
||||
|
||||
foreach ($reversed as $msg) {
|
||||
$content = $msg['content'] ?? '';
|
||||
$tokenEst = (int)(mb_strlen($content) / 3.5); // rough token estimate
|
||||
|
||||
// Skip low-value messages
|
||||
if (wcp_is_low_value($content, $msg['role'] ?? 'user')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$totalEstTokens += $tokenEst;
|
||||
if ($totalEstTokens > $maxTokenEstimate) {
|
||||
// Summarize remaining old messages
|
||||
$remaining = array_slice($reversed, count($snipped));
|
||||
if (!empty($remaining)) {
|
||||
$summary = wcp_summarize_block($remaining);
|
||||
if ($summary) {
|
||||
$snipped[] = ["role" => "system", "content" => "[Résumé conversation précédente: $summary]"];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$snipped[] = $msg;
|
||||
}
|
||||
|
||||
return array_reverse($snipped);
|
||||
}
|
||||
|
||||
function wcp_is_low_value($content, $role) {
|
||||
$l = mb_strtolower(trim($content));
|
||||
$len = mb_strlen($l);
|
||||
|
||||
// Very short acknowledgments
|
||||
if ($len < 15 && preg_match('/^(ok|oui|non|merci|d.accord|parfait|super|cool|top|nice|yes|no|thanks|got it|compris|entendu|noté)$/i', $l)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pure greetings without substance
|
||||
if ($len < 30 && preg_match('/^(bonjour|bonsoir|salut|hello|hi|hey|salam|coucou)\s*(claude|wevia|!|\.|\?)?$/i', $l)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function wcp_summarize_block($messages) {
|
||||
/**
|
||||
* Generate a compact summary of a conversation block.
|
||||
* Uses extractive summarization (key phrases) not LLM calls.
|
||||
*/
|
||||
$topics = [];
|
||||
foreach ($messages as $msg) {
|
||||
$content = $msg['content'] ?? '';
|
||||
// Extract key entities/topics via simple NLP
|
||||
if (preg_match_all('/(?:SAP|ERP|WEVIA|Arsenal|Ethica|API|server|docker|nginx|postgresql|kubernetes|python|php|react|audit|compliance|RGPD|cybersécurité|pharma|marketing|CRM|pipeline)/i', $content, $matches)) {
|
||||
$topics = array_merge($topics, array_map('strtolower', $matches[0]));
|
||||
}
|
||||
}
|
||||
|
||||
$topicCounts = array_count_values($topics);
|
||||
arsort($topicCounts);
|
||||
$topTopics = array_slice(array_keys($topicCounts), 0, 5);
|
||||
|
||||
if (empty($topTopics)) return null;
|
||||
|
||||
$msgCount = count($messages);
|
||||
return "Discussion de $msgCount échanges couvrant: " . implode(', ', $topTopics);
|
||||
}
|
||||
|
||||
function wcp_auto_compact($history, $targetTokens = 4000) {
|
||||
/**
|
||||
* Layer 2: Aggressive summarization when context is critical.
|
||||
* Keeps last 4 exchanges intact, summarizes everything else.
|
||||
*/
|
||||
if (count($history) <= 8) return $history; // 4 exchanges = 8 messages
|
||||
|
||||
$recent = array_slice($history, -8);
|
||||
$old = array_slice($history, 0, -8);
|
||||
|
||||
$summary = wcp_summarize_block($old);
|
||||
$compacted = [];
|
||||
|
||||
if ($summary) {
|
||||
$compacted[] = ["role" => "system", "content" => "[Contexte précédent: $summary]"];
|
||||
}
|
||||
|
||||
return array_merge($compacted, $recent);
|
||||
}
|
||||
|
||||
function wcp_context_collapse($sys, $kbContext, $history, $providerMaxTokens = 32000) {
|
||||
/**
|
||||
* Layer 3: Restructure entire context for provider-specific limits.
|
||||
* Different providers have different context windows:
|
||||
* - Cerebras qwen-3-235b: 8K-32K
|
||||
* - Groq llama-3.3-70b: 128K
|
||||
* - Ollama local qwen3:4b: 8K
|
||||
* - Gemini 2.0 flash: 1M
|
||||
*
|
||||
* Collapse strategy: sys + kb + history must fit in providerMaxTokens
|
||||
*/
|
||||
$sysTokens = (int)(mb_strlen($sys) / 3.5);
|
||||
$kbTokens = (int)(mb_strlen($kbContext) / 3.5);
|
||||
$histTokens = 0;
|
||||
foreach ($history as $h) {
|
||||
$histTokens += (int)(mb_strlen($h['content'] ?? '') / 3.5);
|
||||
}
|
||||
|
||||
$totalTokens = $sysTokens + $kbTokens + $histTokens;
|
||||
|
||||
if ($totalTokens <= $providerMaxTokens * 0.85) {
|
||||
return ['sys' => $sys, 'kb' => $kbContext, 'history' => $history]; // Fits fine
|
||||
}
|
||||
|
||||
error_log("WCP_COLLAPSE: total={$totalTokens} limit={$providerMaxTokens} — compressing");
|
||||
|
||||
// Step 1: Trim KB context (least important)
|
||||
if ($kbTokens > $providerMaxTokens * 0.2) {
|
||||
$maxKbChars = (int)($providerMaxTokens * 0.2 * 3.5);
|
||||
$kbContext = mb_substr($kbContext, 0, $maxKbChars) . "\n[...KB tronquée pour limites contexte...]";
|
||||
}
|
||||
|
||||
// Step 2: Compact history
|
||||
$history = wcp_snip_compact($history, (int)($providerMaxTokens * 0.4));
|
||||
|
||||
// Step 3: If still too large, aggressive compact
|
||||
$newTotal = (int)(mb_strlen($sys) / 3.5) + (int)(mb_strlen($kbContext) / 3.5);
|
||||
foreach ($history as $h) $newTotal += (int)(mb_strlen($h['content'] ?? '') / 3.5);
|
||||
|
||||
if ($newTotal > $providerMaxTokens * 0.85) {
|
||||
$history = wcp_auto_compact($history, (int)($providerMaxTokens * 0.3));
|
||||
}
|
||||
|
||||
// Step 4: UNIVERSAL safe cap — brain's fallback chain may reach smaller providers
|
||||
// Nuclear trim if sys prompt > 6K chars (safe for ALL providers including Cerebras 8K)
|
||||
if (mb_strlen($sys) > 6000) {
|
||||
$sys = mb_substr($sys, 0, 4000) . "\n[...expertises condensées pour optimisation contexte...]\n" . mb_substr($sys, -2000);
|
||||
error_log("WCP_COLLAPSE: UNIVERSAL cap sys=" . mb_strlen($sys) . " (was >" . 6000 . ")");
|
||||
}
|
||||
if ($newTotal > $providerMaxTokens * 0.9) {
|
||||
// Keep core identity + security, trim expertise details
|
||||
if (preg_match('/EXPERTISE [A-Z]+:/s', $sys)) {
|
||||
// Remove all EXPERTISE blocks but keep CONTACT and SECURITE INTERNE
|
||||
$sys = preg_replace('/EXPERTISE (?:SECURITE|PHARMA|ERP|PDF|SCHEMA|API|FRONTEND|DEVOPS|RGPD|IA ETHICS)[^E]+((?=EXPERTISE)|(?=CONTACT)|(?=SECURITE INTERNE)|(?=QUALITE))/s', "[Expertise condensée]\n", $sys);
|
||||
error_log("WCP_COLLAPSE: Trimmed EXPERTISE blocks");
|
||||
}
|
||||
}
|
||||
|
||||
return ['sys' => $sys, 'kb' => $kbContext, 'history' => $history];
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 3. PROVIDER EFFORT LEVELS
|
||||
// Map intent complexity to provider tiers.
|
||||
// Pattern from Claude Code: low/medium/high effort → model selection
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Provider capability tiers for WEVIA's multi-provider fallback
|
||||
*/
|
||||
define('WCP_TIER_FAST', 'fast'); // Simple/greetings: cheapest, fastest provider
|
||||
define('WCP_TIER_BALANCED', 'balanced'); // General: good balance of speed + quality
|
||||
define('WCP_TIER_DEEP', 'deep'); // Complex reasoning: best available model
|
||||
define('WCP_TIER_SPECIALIST', 'specialist'); // Domain-specific: vision, code, etc.
|
||||
|
||||
function wcp_provider_tiers() {
|
||||
return [
|
||||
WCP_TIER_FAST => ['cerebras', 'groq', 'sambanova', 'ollama_s95'],
|
||||
WCP_TIER_BALANCED => ['groq', 'alibaba', 'sambanova', 'mistral', 'deepseek'], // cerebras=8K too small for full sys
|
||||
WCP_TIER_DEEP => ['groq_deep', 'deepseek', 'gemini', 'cohere', 'alibaba'], // no cerebras=8K
|
||||
WCP_TIER_SPECIALIST => [
|
||||
'vision' => ['gemini', 'groq_vision'],
|
||||
'code' => ['cerebras', 'deepseek'],
|
||||
'rag' => ['cohere', 'groq_deep'],
|
||||
'multilingual' => ['mistral', 'alibaba', 'gemini'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function wcp_classify_effort($intent, $msg, $mode = 'balanced') {
|
||||
/**
|
||||
* Determine effort level based on intent + message complexity.
|
||||
* Returns: [tier, priority_providers[], context_budget]
|
||||
*/
|
||||
$msgLen = mb_strlen($msg);
|
||||
$tiers = wcp_provider_tiers();
|
||||
|
||||
// Fast tier: greetings, short messages, acknowledgments
|
||||
if ($intent === 'greeting' || $mode === 'fast' || $msgLen < 20) {
|
||||
return [
|
||||
'tier' => WCP_TIER_FAST,
|
||||
'providers' => $tiers[WCP_TIER_FAST],
|
||||
'context_budget' => 4000,
|
||||
'max_tokens' => 512,
|
||||
];
|
||||
}
|
||||
|
||||
// Specialist tier: domain-specific intents
|
||||
if ($intent === 'image') {
|
||||
return ['tier' => WCP_TIER_SPECIALIST, 'providers' => $tiers[WCP_TIER_SPECIALIST]['vision'], 'context_budget' => 8000, 'max_tokens' => 2048];
|
||||
}
|
||||
if ($intent === 'code' || $intent === 'schema') {
|
||||
return ['tier' => WCP_TIER_SPECIALIST, 'providers' => $tiers[WCP_TIER_SPECIALIST]['code'], 'context_budget' => 16000, 'max_tokens' => 4096];
|
||||
}
|
||||
|
||||
// Deep tier: complex reasoning, analysis, compliance, medical
|
||||
if (in_array($intent, ['analysis', 'compliance', 'medical', 'consulting']) || $mode === 'verified') {
|
||||
return [
|
||||
'tier' => WCP_TIER_DEEP,
|
||||
'providers' => $tiers[WCP_TIER_DEEP],
|
||||
'context_budget' => 24000,
|
||||
'max_tokens' => 4096,
|
||||
];
|
||||
}
|
||||
|
||||
// Long messages suggest complex queries
|
||||
if ($msgLen > 300) {
|
||||
return [
|
||||
'tier' => WCP_TIER_DEEP,
|
||||
'providers' => $tiers[WCP_TIER_DEEP],
|
||||
'context_budget' => 16000,
|
||||
'max_tokens' => 4096,
|
||||
];
|
||||
}
|
||||
|
||||
// Balanced tier: everything else
|
||||
return [
|
||||
'tier' => WCP_TIER_BALANCED,
|
||||
'providers' => $tiers[WCP_TIER_BALANCED],
|
||||
'context_budget' => 12000,
|
||||
'max_tokens' => 2048,
|
||||
];
|
||||
}
|
||||
|
||||
function wcp_smart_route_v3($msg, $mode, $intent, $cbState = []) {
|
||||
/**
|
||||
* Enhanced smartRoute using effort levels + circuit breaker state.
|
||||
* Falls through providers in tier order, skipping broken ones.
|
||||
*/
|
||||
$effort = wcp_classify_effort($intent, $msg, $mode);
|
||||
$providers = $effort['providers'];
|
||||
|
||||
// Filter out circuit-broken providers
|
||||
foreach ($providers as $i => $p) {
|
||||
if (isset($cbState[$p]) && $cbState[$p]['blocked']) {
|
||||
unset($providers[$i]);
|
||||
}
|
||||
}
|
||||
$providers = array_values($providers);
|
||||
|
||||
if (empty($providers)) {
|
||||
// All providers in tier are broken — fallback to ollama
|
||||
error_log("WCP_ROUTE: all tier={$effort['tier']} providers broken, fallback ollama");
|
||||
return ['provider' => 'ollama_s95', 'effort' => $effort];
|
||||
}
|
||||
|
||||
error_log("WCP_ROUTE: tier={$effort['tier']} provider={$providers[0]} budget={$effort['context_budget']}");
|
||||
return ['provider' => $providers[0], 'effort' => $effort];
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 4. DEFERRED TOOL DISCOVERY
|
||||
// Only surface core tools in the base prompt.
|
||||
// Load specialized tools on-demand when the model requests them.
|
||||
// This reduces prompt token usage by ~85%.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wcp_tool_registry() {
|
||||
/**
|
||||
* Complete tool catalog with metadata.
|
||||
* core=true means always present in prompt.
|
||||
* core=false means loaded via deferred discovery.
|
||||
*/
|
||||
return [
|
||||
// ═══ CORE TOOLS (always in prompt) ═══
|
||||
'web_search' => ['core' => true, 'safety' => 'read', 'desc' => 'Recherche web en temps réel'],
|
||||
'kb_search' => ['core' => true, 'safety' => 'read', 'desc' => 'Recherche base de connaissances WEVIA'],
|
||||
'mermaid' => ['core' => true, 'safety' => 'read', 'desc' => 'Génération de schémas Mermaid'],
|
||||
'pdf_generate' => ['core' => true, 'safety' => 'write', 'desc' => 'Génération de documents PDF'],
|
||||
'code_execute' => ['core' => true, 'safety' => 'write', 'desc' => 'Exécution de code Python/PHP'],
|
||||
'image_generate' => ['core' => true, 'safety' => 'write', 'desc' => 'Génération d\'images IA'],
|
||||
'svg_logo' => ['core' => true, 'safety' => 'write', 'desc' => 'Génération de logos SVG'],
|
||||
'tool_search' => ['core' => true, 'safety' => 'read', 'desc' => 'Découvrir des outils spécialisés disponibles'],
|
||||
|
||||
// ═══ SPECIALIZED TOOLS (loaded on demand) ═══
|
||||
'nuclei_scan' => ['core' => false, 'safety' => 'write', 'category' => 'security', 'desc' => 'Scan de vulnérabilités Nuclei'],
|
||||
'kilo_cli' => ['core' => false, 'safety' => 'write', 'category' => 'devops', 'desc' => 'Kilo CLI pour gestion serveurs'],
|
||||
'toolFK' => ['core' => false, 'safety' => 'read', 'category' => 'data', 'desc' => '25 outils data: JSON, CSV, XML, regex, hash, etc.'],
|
||||
'ethica_hcp' => ['core' => false, 'safety' => 'read', 'category' => 'pharma', 'desc' => 'Recherche HCP base Ethica'],
|
||||
'arsenal_query' => ['core' => false, 'safety' => 'write', 'category' => 'marketing', 'desc' => 'Requêtes Arsenal WEVADS'],
|
||||
'domain_check' => ['core' => false, 'safety' => 'read', 'category' => 'marketing', 'desc' => 'Vérification domaine email (DKIM/SPF/DMARC)'],
|
||||
'cron_manager' => ['core' => false, 'safety' => 'write', 'category' => 'devops', 'desc' => 'Gestion des crons serveur'],
|
||||
'db_query' => ['core' => false, 'safety' => 'read', 'category' => 'data', 'desc' => 'Requêtes PostgreSQL en lecture'],
|
||||
'git_status' => ['core' => false, 'safety' => 'read', 'category' => 'devops', 'desc' => 'Statut des repos Git'],
|
||||
'docker_status' => ['core' => false, 'safety' => 'read', 'category' => 'devops', 'desc' => 'Statut des conteneurs Docker'],
|
||||
'uptime_check' => ['core' => false, 'safety' => 'read', 'category' => 'monitoring', 'desc' => 'Statut Uptime Kuma'],
|
||||
'plausible_stats' => ['core' => false, 'safety' => 'read', 'category' => 'analytics', 'desc' => 'Statistiques Plausible Analytics'],
|
||||
'searxng' => ['core' => false, 'safety' => 'read', 'category' => 'search', 'desc' => 'Recherche SearXNG souveraine'],
|
||||
'n8n_workflow' => ['core' => false, 'safety' => 'write', 'category' => 'automation', 'desc' => 'Déclenchement workflows n8n'],
|
||||
'warmup_control' => ['core' => false, 'safety' => 'write', 'category' => 'marketing', 'desc' => 'Contrôle warmup emails'],
|
||||
'contact_import' => ['core' => false, 'safety' => 'write', 'category' => 'marketing', 'desc' => 'Import contacts CSV'],
|
||||
'ai_template' => ['core' => false, 'safety' => 'write', 'category' => 'marketing', 'desc' => 'Génération templates email IA'],
|
||||
'rlhf_feedback' => ['core' => false, 'safety' => 'write', 'category' => 'system', 'desc' => 'Feedback RLHF utilisateur'],
|
||||
];
|
||||
}
|
||||
|
||||
function wcp_get_core_tools() {
|
||||
/**
|
||||
* Return only core tools for the base prompt.
|
||||
* Sorted alphabetically for prompt cache stability.
|
||||
*/
|
||||
$registry = wcp_tool_registry();
|
||||
$core = array_filter($registry, fn($t) => $t['core'] === true);
|
||||
ksort($core); // Alphabetical sort for cache stability
|
||||
return $core;
|
||||
}
|
||||
|
||||
function wcp_discover_tools($query) {
|
||||
/**
|
||||
* Deferred tool discovery: search specialized tools by keyword.
|
||||
* Called when the model uses the tool_search meta-tool.
|
||||
*/
|
||||
$registry = wcp_tool_registry();
|
||||
$specialized = array_filter($registry, fn($t) => $t['core'] === false);
|
||||
|
||||
$query = mb_strtolower($query);
|
||||
$results = [];
|
||||
|
||||
foreach ($specialized as $name => $tool) {
|
||||
$searchable = mb_strtolower($name . ' ' . $tool['desc'] . ' ' . ($tool['category'] ?? ''));
|
||||
if (mb_strpos($searchable, $query) !== false) {
|
||||
$results[$name] = $tool;
|
||||
}
|
||||
}
|
||||
|
||||
// Also match by category
|
||||
foreach ($specialized as $name => $tool) {
|
||||
if (isset($tool['category']) && mb_strpos(mb_strtolower($tool['category']), $query) !== false) {
|
||||
$results[$name] = $tool;
|
||||
}
|
||||
}
|
||||
|
||||
ksort($results); // Alphabetical for cache stability
|
||||
return $results;
|
||||
}
|
||||
|
||||
function wcp_tool_discovery_prompt() {
|
||||
/**
|
||||
* Generate the tool_search instruction for the system prompt.
|
||||
* This replaces listing all 25+ tools.
|
||||
*/
|
||||
$core = wcp_get_core_tools();
|
||||
$specialized = array_filter(wcp_tool_registry(), fn($t) => $t['core'] === false);
|
||||
$categories = array_unique(array_filter(array_map(fn($t) => $t['category'] ?? null, $specialized)));
|
||||
sort($categories);
|
||||
|
||||
$prompt = "OUTILS DISPONIBLES:\n";
|
||||
foreach ($core as $name => $tool) {
|
||||
$prompt .= "- $name: {$tool['desc']}\n";
|
||||
}
|
||||
$prompt .= "\nOUTILS SPÉCIALISÉS (utilise tool_search pour les découvrir):\n";
|
||||
$prompt .= "Catégories: " . implode(', ', $categories) . "\n";
|
||||
$prompt .= "Pour accéder aux outils spécialisés, utilise: tool_search(\"catégorie ou mot-clé\")\n";
|
||||
$prompt .= "Exemple: tool_search(\"security\") → nuclei_scan, etc.\n";
|
||||
|
||||
return $prompt;
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 5. ENHANCED CIRCUIT BREAKER
|
||||
// Per-provider health tracking with exponential decay.
|
||||
// Pattern: track failures, auto-recover after cooldown.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wcp_cb_state_file() {
|
||||
return '/tmp/wcp_circuit_breaker.json';
|
||||
}
|
||||
|
||||
function wcp_cb_load() {
|
||||
$file = wcp_cb_state_file();
|
||||
if (!file_exists($file)) return [];
|
||||
$data = @json_decode(file_get_contents($file), true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
function wcp_cb_save($state) {
|
||||
file_put_contents(wcp_cb_state_file(), json_encode($state));
|
||||
}
|
||||
|
||||
function wcp_cb_record_failure($provider) {
|
||||
$state = wcp_cb_load();
|
||||
if (!isset($state[$provider])) {
|
||||
$state[$provider] = ['failures' => 0, 'last_failure' => 0, 'blocked' => false, 'blocked_until' => 0];
|
||||
}
|
||||
|
||||
$state[$provider]['failures']++;
|
||||
$state[$provider]['last_failure'] = time();
|
||||
|
||||
// Block after 3 consecutive failures
|
||||
if ($state[$provider]['failures'] >= 3) {
|
||||
// Exponential backoff: 30s, 60s, 120s, 240s, max 600s
|
||||
$cooldown = min(600, 30 * pow(2, $state[$provider]['failures'] - 3));
|
||||
$state[$provider]['blocked'] = true;
|
||||
$state[$provider]['blocked_until'] = time() + $cooldown;
|
||||
error_log("WCP_CB: BLOCKED provider=$provider failures={$state[$provider]['failures']} cooldown={$cooldown}s");
|
||||
}
|
||||
|
||||
wcp_cb_save($state);
|
||||
return $state;
|
||||
}
|
||||
|
||||
function wcp_cb_record_success($provider) {
|
||||
$state = wcp_cb_load();
|
||||
if (isset($state[$provider])) {
|
||||
// Decay failures on success (don't reset to 0 immediately)
|
||||
$state[$provider]['failures'] = max(0, $state[$provider]['failures'] - 1);
|
||||
$state[$provider]['blocked'] = false;
|
||||
$state[$provider]['blocked_until'] = 0;
|
||||
}
|
||||
wcp_cb_save($state);
|
||||
return $state;
|
||||
}
|
||||
|
||||
function wcp_cb_is_available($provider) {
|
||||
$state = wcp_cb_load();
|
||||
if (!isset($state[$provider])) return true;
|
||||
|
||||
$p = $state[$provider];
|
||||
if (!$p['blocked']) return true;
|
||||
|
||||
// Auto-recover after cooldown
|
||||
if (time() > $p['blocked_until']) {
|
||||
$state[$provider]['blocked'] = false;
|
||||
wcp_cb_save($state);
|
||||
error_log("WCP_CB: AUTO_RECOVER provider=$provider");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function wcp_cb_available_providers($providerList) {
|
||||
return array_filter($providerList, fn($p) => wcp_cb_is_available($p));
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 6. CONCURRENCY SAFETY TIERS
|
||||
// Classification of tools as read-only (safe for parallel)
|
||||
// vs write (must be serial). From Claude Code pattern.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wcp_tool_safety($toolName) {
|
||||
$registry = wcp_tool_registry();
|
||||
return $registry[$toolName]['safety'] ?? 'write'; // Default to write (safe)
|
||||
}
|
||||
|
||||
function wcp_can_parallel($tools) {
|
||||
/**
|
||||
* Check if a set of tools can all run in parallel.
|
||||
* Only if ALL tools are read-only.
|
||||
*/
|
||||
foreach ($tools as $tool) {
|
||||
if (wcp_tool_safety($tool) !== 'read') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 7. MEMORY CONSOLIDATION (DREAM SYSTEM)
|
||||
// Background process that consolidates session memories.
|
||||
// Phases: Orient → Gather → Consolidate → Prune
|
||||
// Designed to run as a cron job.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wcp_dream_consolidate($pdo, $sessionId = null) {
|
||||
/**
|
||||
* Dream consolidation for WEVIA session memory.
|
||||
*
|
||||
* Phase 1 - Orient: Load existing memory file
|
||||
* Phase 2 - Gather: Extract key facts from recent sessions
|
||||
* Phase 3 - Consolidate: Merge, deduplicate, resolve contradictions
|
||||
* Phase 4 - Prune: Keep under WCP_MAX_MEMORY_LINES (200)
|
||||
*
|
||||
* Storage: admin.wevia_memory table
|
||||
*/
|
||||
|
||||
// Ensure memory table exists
|
||||
try {
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.wevia_memory (
|
||||
id SERIAL PRIMARY KEY,
|
||||
session_id VARCHAR(100),
|
||||
category VARCHAR(50) DEFAULT 'general',
|
||||
fact TEXT NOT NULL,
|
||||
confidence FLOAT DEFAULT 1.0,
|
||||
source VARCHAR(100),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
contradicted_by INT REFERENCES admin.wevia_memory(id),
|
||||
active BOOLEAN DEFAULT true
|
||||
)");
|
||||
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_wevia_memory_active ON admin.wevia_memory(active) WHERE active = true");
|
||||
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_wevia_memory_session ON admin.wevia_memory(session_id)");
|
||||
} catch(\Exception $e) {
|
||||
error_log("WCP_DREAM: table creation error: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Phase 1: Orient — Load current memories
|
||||
$currentMemories = [];
|
||||
try {
|
||||
$st = $pdo->query("SELECT id, category, fact, confidence, created_at FROM admin.wevia_memory WHERE active = true ORDER BY confidence DESC, updated_at DESC LIMIT 200");
|
||||
$currentMemories = $st->fetchAll(\PDO::FETCH_ASSOC);
|
||||
} catch(\Exception $e) {
|
||||
error_log("WCP_DREAM: orient error: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Phase 2: Gather — Extract facts from recent RLHF/conversations
|
||||
$recentFacts = [];
|
||||
try {
|
||||
// Get recent conversations that haven't been consolidated
|
||||
$st = $pdo->query("SELECT question, answer, provider, created_at FROM admin.rlhf_feedback
|
||||
WHERE created_at > NOW() - INTERVAL '7 days'
|
||||
ORDER BY created_at DESC LIMIT 100");
|
||||
$conversations = $st->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($conversations as $conv) {
|
||||
$extracted = wcp_extract_facts($conv['question'], $conv['answer']);
|
||||
foreach ($extracted as $fact) {
|
||||
$recentFacts[] = array_merge($fact, ['source' => 'rlhf_' . substr(md5($conv['created_at']), 0, 8)]);
|
||||
}
|
||||
}
|
||||
} catch(\Exception $e) {
|
||||
error_log("WCP_DREAM: gather error: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Phase 3: Consolidate — Merge with existing, resolve contradictions
|
||||
$consolidated = wcp_merge_memories($currentMemories, $recentFacts, $pdo);
|
||||
|
||||
// Phase 4: Prune — Keep under 200 lines
|
||||
$pruned = wcp_prune_memories($consolidated, WCP_MAX_MEMORY_LINES);
|
||||
|
||||
error_log("WCP_DREAM: consolidated " . count($recentFacts) . " new facts, total active=" . count($pruned));
|
||||
|
||||
return ['new_facts' => count($recentFacts), 'total' => count($pruned), 'pruned' => count($consolidated) - count($pruned)];
|
||||
}
|
||||
|
||||
function wcp_extract_facts($question, $answer) {
|
||||
/**
|
||||
* Extract factual claims from a Q&A exchange.
|
||||
* Simple extractive approach — no LLM needed.
|
||||
*/
|
||||
$facts = [];
|
||||
$combined = $question . ' ' . $answer;
|
||||
|
||||
// Extract technology mentions
|
||||
if (preg_match_all('/(utilise|recommande|implementé|déployé|configuré)\s+(\w+(?:\s+\w+)?)/iu', $combined, $m)) {
|
||||
foreach ($m[0] as $match) {
|
||||
$facts[] = ['category' => 'technology', 'fact' => trim($match), 'confidence' => 0.7];
|
||||
}
|
||||
}
|
||||
|
||||
// Extract decisions
|
||||
if (preg_match_all('/(choisi|décidé|opté pour|validé|confirmé)\s+(.+?)[\.\,\;]/iu', $combined, $m)) {
|
||||
foreach ($m[0] as $match) {
|
||||
$facts[] = ['category' => 'decision', 'fact' => trim($match), 'confidence' => 0.9];
|
||||
}
|
||||
}
|
||||
|
||||
// Extract problems/solutions
|
||||
if (preg_match_all('/(problème|erreur|bug|fix|solution|résolu)\s*:?\s*(.{20,100})/iu', $combined, $m)) {
|
||||
foreach ($m[0] as $match) {
|
||||
$facts[] = ['category' => 'troubleshoot', 'fact' => trim($match), 'confidence' => 0.8];
|
||||
}
|
||||
}
|
||||
|
||||
return $facts;
|
||||
}
|
||||
|
||||
function wcp_merge_memories($existing, $newFacts, $pdo) {
|
||||
/**
|
||||
* Merge new facts with existing memories.
|
||||
* Detect duplicates via fuzzy matching.
|
||||
* Resolve contradictions (newer wins, mark old as contradicted).
|
||||
*/
|
||||
$merged = $existing;
|
||||
|
||||
foreach ($newFacts as $fact) {
|
||||
$isDuplicate = false;
|
||||
|
||||
foreach ($existing as $mem) {
|
||||
// Simple fuzzy match: >70% shared words
|
||||
$simil = wcp_text_similarity($fact['fact'], $mem['fact']);
|
||||
if ($simil > 0.7) {
|
||||
$isDuplicate = true;
|
||||
// Update confidence if new is higher
|
||||
if ($fact['confidence'] > $mem['confidence']) {
|
||||
try {
|
||||
$pdo->prepare("UPDATE admin.wevia_memory SET confidence = ?, updated_at = NOW() WHERE id = ?")
|
||||
->execute([$fact['confidence'], $mem['id']]);
|
||||
} catch(\Exception $e) {}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isDuplicate) {
|
||||
// Insert new fact
|
||||
try {
|
||||
$st = $pdo->prepare("INSERT INTO admin.wevia_memory (category, fact, confidence, source) VALUES (?, ?, ?, ?)");
|
||||
$st->execute([$fact['category'], $fact['fact'], $fact['confidence'], $fact['source'] ?? 'dream']);
|
||||
$merged[] = $fact;
|
||||
} catch(\Exception $e) {
|
||||
error_log("WCP_DREAM: insert error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
function wcp_prune_memories($memories, $maxLines) {
|
||||
/**
|
||||
* Keep only top N memories by confidence * recency.
|
||||
* Deactivate old/low-confidence ones.
|
||||
*/
|
||||
// Already within limits
|
||||
if (count($memories) <= $maxLines) return $memories;
|
||||
|
||||
// Sort by confidence DESC, created_at DESC
|
||||
usort($memories, function($a, $b) {
|
||||
$confDiff = ($b['confidence'] ?? 0.5) - ($a['confidence'] ?? 0.5);
|
||||
if (abs($confDiff) > 0.01) return $confDiff > 0 ? 1 : -1;
|
||||
return strcmp($b['created_at'] ?? '', $a['created_at'] ?? '');
|
||||
});
|
||||
|
||||
return array_slice($memories, 0, $maxLines);
|
||||
}
|
||||
|
||||
function wcp_text_similarity($a, $b) {
|
||||
$wordsA = array_unique(preg_split('/\s+/', mb_strtolower($a)));
|
||||
$wordsB = array_unique(preg_split('/\s+/', mb_strtolower($b)));
|
||||
|
||||
if (empty($wordsA) || empty($wordsB)) return 0;
|
||||
|
||||
$intersection = count(array_intersect($wordsA, $wordsB));
|
||||
$union = count(array_unique(array_merge($wordsA, $wordsB)));
|
||||
|
||||
return $union > 0 ? $intersection / $union : 0;
|
||||
}
|
||||
|
||||
function wcp_get_session_memory($pdo, $limit = 20) {
|
||||
/**
|
||||
* Load active memories for injection into system prompt.
|
||||
* Returns formatted string for prompt enrichment.
|
||||
*/
|
||||
try {
|
||||
$st = $pdo->query("SELECT category, fact FROM admin.wevia_memory
|
||||
WHERE active = true
|
||||
ORDER BY confidence DESC, updated_at DESC
|
||||
LIMIT $limit");
|
||||
$memories = $st->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($memories)) return '';
|
||||
|
||||
$grouped = [];
|
||||
foreach ($memories as $m) {
|
||||
$grouped[$m['category']][] = $m['fact'];
|
||||
}
|
||||
|
||||
$prompt = "\nMÉMOIRE CONSOLIDÉE:\n";
|
||||
foreach ($grouped as $cat => $facts) {
|
||||
$prompt .= strtoupper($cat) . ": " . implode('; ', array_slice($facts, 0, 5)) . "\n";
|
||||
}
|
||||
|
||||
return $prompt;
|
||||
} catch(\Exception $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 8. PROVIDER CONTEXT WINDOWS
|
||||
// Map each provider to its actual context window limit.
|
||||
// Used by context_collapse to optimize per-provider.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wcp_provider_context_limits() {
|
||||
return [
|
||||
'cerebras' => 8192,
|
||||
'groq' => 131072,
|
||||
'groq_deep' => 131072,
|
||||
'groq_vision' => 131072,
|
||||
'ollama_s95' => 8192,
|
||||
'ollama_local' => 8192,
|
||||
'mistral' => 32768,
|
||||
'deepseek' => 65536,
|
||||
'alibaba' => 32768,
|
||||
'sambanova' => 65536,
|
||||
'gemini' => 1048576,
|
||||
'cohere' => 131072,
|
||||
];
|
||||
}
|
||||
|
||||
function wcp_get_context_limit($provider) {
|
||||
$limits = wcp_provider_context_limits();
|
||||
return $limits[$provider] ?? 8192; // Conservative default
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 9. CRASH-SAFE PERSISTENCE
|
||||
// Write conversation state before API calls.
|
||||
// Pattern from Claude Code: JSONL format, session resumable.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wcp_persist_turn($sessionId, $role, $content, $provider = '', $metadata = []) {
|
||||
$logDir = '/var/www/weval/wevia-ia/session-logs';
|
||||
if (!is_dir($logDir)) @mkdir($logDir, 0755, true);
|
||||
|
||||
$entry = json_encode([
|
||||
'ts' => date('c'),
|
||||
'session' => $sessionId,
|
||||
'role' => $role,
|
||||
'content' => mb_substr($content, 0, 5000), // Cap for storage
|
||||
'provider' => $provider,
|
||||
'meta' => $metadata,
|
||||
], JSON_UNESCAPED_UNICODE) . "\n";
|
||||
|
||||
@file_put_contents("$logDir/$sessionId.jsonl", $entry, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
function wcp_load_session($sessionId) {
|
||||
$file = "/var/www/weval/wevia-ia/session-logs/$sessionId.jsonl";
|
||||
if (!file_exists($file)) return [];
|
||||
|
||||
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$turns = [];
|
||||
foreach ($lines as $line) {
|
||||
$turn = @json_decode($line, true);
|
||||
if ($turn) $turns[] = $turn;
|
||||
}
|
||||
return $turns;
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// INTEGRATION HELPER
|
||||
// One-call function to wire all patterns into the existing brain.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wcp_enhance_request($msg, $mode, $intent, $history, $sys, $kbContext, $provider, $pdo = null) {
|
||||
/**
|
||||
* Main integration point. Call this before sending to a provider.
|
||||
* Returns enhanced parameters ready for API call.
|
||||
*/
|
||||
|
||||
// 1. Classify effort and get optimal provider
|
||||
$route = wcp_smart_route_v3($msg, $mode, $intent, wcp_cb_load());
|
||||
$selectedProvider = $route['provider'];
|
||||
$effort = $route['effort'];
|
||||
|
||||
// 2. Get provider context limit
|
||||
$contextLimit = wcp_get_context_limit($selectedProvider);
|
||||
|
||||
// 3. Apply three-layer context compression
|
||||
$compressed = wcp_context_collapse($sys, $kbContext, $history, $contextLimit);
|
||||
|
||||
// 4. Inject session memory if available
|
||||
if ($pdo) {
|
||||
$memory = wcp_get_session_memory($pdo, 10);
|
||||
if ($memory) {
|
||||
$compressed['sys'] .= $memory;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Add tool discovery prompt
|
||||
$compressed['sys'] .= "\n" . wcp_tool_discovery_prompt();
|
||||
|
||||
// 6. Persist turn for crash safety
|
||||
$sessionId = session_id() ?: md5(microtime());
|
||||
wcp_persist_turn($sessionId, 'user', $msg, $selectedProvider);
|
||||
|
||||
return [
|
||||
'provider' => $selectedProvider,
|
||||
'effort' => $effort,
|
||||
'sys' => $compressed['sys'],
|
||||
'kb' => $compressed['kb'],
|
||||
'history' => $compressed['history'],
|
||||
'context_limit' => $contextLimit,
|
||||
'max_tokens' => $effort['max_tokens'],
|
||||
'session_id' => $sessionId,
|
||||
];
|
||||
}
|
||||
|
||||
// Log module load
|
||||
error_log("WCP: wevia-claude-code-patterns v" . WCP_VERSION . " loaded (" . count(wcp_tool_registry()) . " tools registered)");
|
||||
349
api/wevia-mcp-layer.php
Normal file
349
api/wevia-mcp-layer.php
Normal file
@@ -0,0 +1,349 @@
|
||||
<?php
|
||||
/**
|
||||
* WEVIA MCP Layer v1.0 — Model Context Protocol
|
||||
* Pattern #17 from Claude Code architecture
|
||||
*
|
||||
* Dual mode:
|
||||
* SERVER: Exposes WEVIA tools as MCP endpoints (JSON-RPC 2.0)
|
||||
* CLIENT: Consumes external MCP servers (tool discovery + invocation)
|
||||
*
|
||||
* Spec: https://modelcontextprotocol.io
|
||||
*
|
||||
* SERVER endpoint: /api/mcp.php (this file)
|
||||
* CLIENT: wevia_mcp_discover() + wevia_mcp_invoke()
|
||||
*
|
||||
* Usage: require_once __DIR__ . '/wevia-mcp-layer.php';
|
||||
*/
|
||||
|
||||
define('MCP_VERSION', '1.0.0');
|
||||
define('MCP_PROTOCOL', '2024-11-05'); // MCP spec version
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// MCP SERVER — Expose WEVIA tools as MCP endpoints
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function mcp_server_handle($request) {
|
||||
$method = $request['method'] ?? '';
|
||||
$params = $request['params'] ?? [];
|
||||
$id = $request['id'] ?? null;
|
||||
|
||||
switch ($method) {
|
||||
case 'initialize':
|
||||
return mcp_response($id, [
|
||||
'protocolVersion' => MCP_PROTOCOL,
|
||||
'capabilities' => [
|
||||
'tools' => ['listChanged' => false],
|
||||
'resources' => ['subscribe' => false, 'listChanged' => false],
|
||||
],
|
||||
'serverInfo' => [
|
||||
'name' => 'wevia-mcp-server',
|
||||
'version' => MCP_VERSION,
|
||||
'description' => 'WEVIA Sovereign AI — Tool Server'
|
||||
]
|
||||
]);
|
||||
|
||||
case 'tools/list':
|
||||
return mcp_response($id, ['tools' => mcp_list_tools()]);
|
||||
|
||||
case 'tools/call':
|
||||
$toolName = $params['name'] ?? '';
|
||||
$toolArgs = $params['arguments'] ?? [];
|
||||
return mcp_response($id, mcp_call_tool($toolName, $toolArgs));
|
||||
|
||||
case 'resources/list':
|
||||
return mcp_response($id, ['resources' => mcp_list_resources()]);
|
||||
|
||||
case 'resources/read':
|
||||
$uri = $params['uri'] ?? '';
|
||||
return mcp_response($id, mcp_read_resource($uri));
|
||||
|
||||
case 'ping':
|
||||
return mcp_response($id, []);
|
||||
|
||||
default:
|
||||
return mcp_error($id, -32601, "Method not found: $method");
|
||||
}
|
||||
}
|
||||
|
||||
function mcp_list_tools() {
|
||||
return [
|
||||
[
|
||||
'name' => 'wevia_chat',
|
||||
'description' => 'Send a message to WEVIA sovereign AI chatbot',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'message' => ['type' => 'string', 'description' => 'User message'],
|
||||
'mode' => ['type' => 'string', 'enum' => ['fast', 'balanced', 'verified'], 'default' => 'balanced'],
|
||||
'lang' => ['type' => 'string', 'default' => 'fr'],
|
||||
],
|
||||
'required' => ['message']
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'wevia_kb_search',
|
||||
'description' => 'Search WEVIA knowledge base (32 tables, 2490+ entries)',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'query' => ['type' => 'string', 'description' => 'Search query'],
|
||||
'limit' => ['type' => 'integer', 'default' => 5],
|
||||
],
|
||||
'required' => ['query']
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'wevia_pdf_generate',
|
||||
'description' => 'Generate a professional PDF document on any topic',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'topic' => ['type' => 'string', 'description' => 'Document topic/title'],
|
||||
'sections' => ['type' => 'integer', 'default' => 8],
|
||||
],
|
||||
'required' => ['topic']
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'wevia_mermaid',
|
||||
'description' => 'Generate a Mermaid diagram (flowchart, sequence, class, etc.)',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'description' => ['type' => 'string', 'description' => 'What to diagram'],
|
||||
'type' => ['type' => 'string', 'enum' => ['flowchart', 'sequence', 'class', 'state', 'er', 'gantt'], 'default' => 'flowchart'],
|
||||
],
|
||||
'required' => ['description']
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'wevia_web_search',
|
||||
'description' => 'Sovereign web search via SearXNG',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'query' => ['type' => 'string'],
|
||||
'max_results' => ['type' => 'integer', 'default' => 5],
|
||||
],
|
||||
'required' => ['query']
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'wevia_benchmark',
|
||||
'description' => 'Get AI ecosystem benchmark data (66 AIs, 222 skills)',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'ai_name' => ['type' => 'string', 'description' => 'Specific AI name or "all"'],
|
||||
],
|
||||
'required' => []
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function mcp_call_tool($name, $args) {
|
||||
switch ($name) {
|
||||
case 'wevia_chat':
|
||||
$ch = curl_init('http://127.0.0.1/wevia-ia/weval-chatbot-api.php');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_POSTFIELDS => json_encode([
|
||||
'message' => $args['message'] ?? '',
|
||||
'mode' => $args['mode'] ?? 'balanced',
|
||||
'lang' => $args['lang'] ?? 'fr',
|
||||
])
|
||||
]);
|
||||
$r = curl_exec($ch); curl_close($ch);
|
||||
$d = json_decode($r, true);
|
||||
return ['content' => [['type' => 'text', 'text' => $d['response'] ?? 'No response']]];
|
||||
|
||||
case 'wevia_kb_search':
|
||||
$query = $args['query'] ?? '';
|
||||
$limit = $args['limit'] ?? 5;
|
||||
// Direct KB query
|
||||
try {
|
||||
@require_once('/opt/wevads/vault/credentials.php');
|
||||
$pdo = new PDO("pgsql:host=10.1.0.3;dbname=adx_system", "admin", "admin123");
|
||||
$st = $pdo->prepare("SELECT title, content, category FROM admin.wevia_kb WHERE LOWER(content) LIKE LOWER(?) LIMIT ?");
|
||||
$st->execute(["%$query%", $limit]);
|
||||
$results = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
return ['content' => [['type' => 'text', 'text' => json_encode($results, JSON_UNESCAPED_UNICODE)]]];
|
||||
} catch (Exception $e) {
|
||||
return ['content' => [['type' => 'text', 'text' => 'KB search error: ' . $e->getMessage()]]];
|
||||
}
|
||||
|
||||
case 'wevia_benchmark':
|
||||
$cache = @json_decode(@file_get_contents('/var/www/html/api/ai-benchmark-cache.json'), true);
|
||||
$aiName = $args['ai_name'] ?? 'all';
|
||||
if ($aiName === 'all') {
|
||||
$lb = $cache['leaderboard'] ?? [];
|
||||
arsort($lb);
|
||||
return ['content' => [['type' => 'text', 'text' => json_encode(array_slice($lb, 0, 20, true))]]];
|
||||
}
|
||||
$ai = $cache['all_ais'][$aiName] ?? null;
|
||||
return ['content' => [['type' => 'text', 'text' => $ai ? json_encode($ai) : "AI '$aiName' not found"]]];
|
||||
|
||||
case 'wevia_web_search':
|
||||
$ch = curl_init('http://127.0.0.1:8888/search?q=' . urlencode($args['query'] ?? '') . '&format=json&categories=general');
|
||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10]);
|
||||
$r = curl_exec($ch); curl_close($ch);
|
||||
$d = json_decode($r, true);
|
||||
$results = array_slice($d['results'] ?? [], 0, $args['max_results'] ?? 5);
|
||||
$text = implode("\n\n", array_map(fn($r) => "**{$r['title']}**\n{$r['url']}\n{$r['content']}", $results));
|
||||
return ['content' => [['type' => 'text', 'text' => $text ?: 'No results']]];
|
||||
|
||||
default:
|
||||
return ['content' => [['type' => 'text', 'text' => "Unknown tool: $name"]], 'isError' => true];
|
||||
}
|
||||
}
|
||||
|
||||
function mcp_list_resources() {
|
||||
return [
|
||||
['uri' => 'wevia://benchmark/leaderboard', 'name' => 'AI Benchmark Leaderboard', 'mimeType' => 'application/json'],
|
||||
['uri' => 'wevia://config/providers', 'name' => 'Active Providers', 'mimeType' => 'application/json'],
|
||||
['uri' => 'wevia://status/health', 'name' => 'System Health', 'mimeType' => 'application/json'],
|
||||
];
|
||||
}
|
||||
|
||||
function mcp_read_resource($uri) {
|
||||
switch ($uri) {
|
||||
case 'wevia://benchmark/leaderboard':
|
||||
$cache = @json_decode(@file_get_contents('/var/www/html/api/ai-benchmark-cache.json'), true);
|
||||
return ['contents' => [['uri' => $uri, 'mimeType' => 'application/json', 'text' => json_encode($cache['leaderboard'] ?? [])]]];
|
||||
case 'wevia://config/providers':
|
||||
return ['contents' => [['uri' => $uri, 'mimeType' => 'application/json', 'text' => json_encode(['chain' => 'Groq→Cerebras→SambaNova→Mistral→Alibaba', 'sovereign' => 'Local→EU→Free→Paid'])]]];
|
||||
case 'wevia://status/health':
|
||||
return ['contents' => [['uri' => $uri, 'mimeType' => 'application/json', 'text' => json_encode(['status' => 'ok', 'modules' => ['WCP' => 'v1.0', 'WSI' => 'v1.0', 'Dream' => 'active', 'MCP' => 'v1.0'], 'uptime' => exec('uptime -p 2>/dev/null')])]]];
|
||||
default:
|
||||
return ['contents' => []];
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// MCP CLIENT — Consume external MCP servers
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wevia_mcp_discover($serverUrl, $timeout = 10) {
|
||||
/**
|
||||
* Discover tools from an external MCP server.
|
||||
* Returns array of available tools with schemas.
|
||||
*/
|
||||
// Initialize
|
||||
$init = wevia_mcp_call($serverUrl, 'initialize', [
|
||||
'protocolVersion' => MCP_PROTOCOL,
|
||||
'capabilities' => [],
|
||||
'clientInfo' => ['name' => 'wevia-mcp-client', 'version' => MCP_VERSION]
|
||||
], $timeout);
|
||||
|
||||
if (!$init) return ['error' => 'init_failed'];
|
||||
|
||||
// List tools
|
||||
$tools = wevia_mcp_call($serverUrl, 'tools/list', [], $timeout);
|
||||
return $tools['tools'] ?? [];
|
||||
}
|
||||
|
||||
function wevia_mcp_invoke($serverUrl, $toolName, $arguments = [], $timeout = 30) {
|
||||
/**
|
||||
* Invoke a tool on an external MCP server.
|
||||
*/
|
||||
$result = wevia_mcp_call($serverUrl, 'tools/call', [
|
||||
'name' => $toolName,
|
||||
'arguments' => $arguments,
|
||||
], $timeout);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function wevia_mcp_call($serverUrl, $method, $params = [], $timeout = 10) {
|
||||
$payload = json_encode([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => uniqid('mcp_'),
|
||||
'method' => $method,
|
||||
'params' => $params,
|
||||
]);
|
||||
|
||||
$ch = curl_init($serverUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => $timeout,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'],
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
]);
|
||||
$r = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($code !== 200 || !$r) return null;
|
||||
$d = json_decode($r, true);
|
||||
return $d['result'] ?? null;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// MCP REGISTRY — Track connected MCP servers
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wevia_mcp_registry() {
|
||||
/**
|
||||
* Registry of known MCP servers.
|
||||
* Auto-populated by OSS Discovery + manual config.
|
||||
*/
|
||||
$registryFile = '/opt/wevads/vault/mcp-registry.json';
|
||||
if (!file_exists($registryFile)) {
|
||||
$default = [
|
||||
'servers' => [
|
||||
['name' => 'wevia-local', 'url' => 'http://127.0.0.1/api/mcp.php', 'type' => 'local', 'active' => true],
|
||||
],
|
||||
'last_scan' => null,
|
||||
];
|
||||
@file_put_contents($registryFile, json_encode($default, JSON_PRETTY_PRINT));
|
||||
return $default;
|
||||
}
|
||||
return json_decode(file_get_contents($registryFile), true) ?: [];
|
||||
}
|
||||
|
||||
function wevia_mcp_register_server($name, $url, $type = 'external') {
|
||||
$registry = wevia_mcp_registry();
|
||||
$registry['servers'][] = ['name' => $name, 'url' => $url, 'type' => $type, 'active' => true, 'added' => date('c')];
|
||||
@file_put_contents('/opt/wevads/vault/mcp-registry.json', json_encode($registry, JSON_PRETTY_PRINT));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// JSON-RPC HELPERS
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function mcp_response($id, $result) {
|
||||
return ['jsonrpc' => '2.0', 'id' => $id, 'result' => $result];
|
||||
}
|
||||
|
||||
function mcp_error($id, $code, $message) {
|
||||
return ['jsonrpc' => '2.0', 'id' => $id, 'error' => ['code' => $code, 'message' => $message]];
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// STANDALONE SERVER MODE — if called directly as /api/mcp.php
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
if (basename($_SERVER['SCRIPT_FILENAME'] ?? '') === 'wevia-mcp-layer.php' ||
|
||||
basename($_SERVER['SCRIPT_FILENAME'] ?? '') === 'mcp.php') {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Auth check
|
||||
$key = $_GET['k'] ?? $_SERVER['HTTP_X_MCP_KEY'] ?? '';
|
||||
if ($key !== 'WEVADS2026' && $key !== 'MCP2026') {
|
||||
http_response_code(403);
|
||||
die(json_encode(mcp_error(null, -32000, 'Unauthorized')));
|
||||
}
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$body || !isset($body['method'])) {
|
||||
die(json_encode(mcp_error(null, -32700, 'Parse error')));
|
||||
}
|
||||
|
||||
die(json_encode(mcp_server_handle($body)));
|
||||
}
|
||||
|
||||
error_log("MCP: wevia-mcp-layer v" . MCP_VERSION . " loaded (server+client)");
|
||||
381
api/wevia-sovereign-intelligence.php
Normal file
381
api/wevia-sovereign-intelligence.php
Normal file
@@ -0,0 +1,381 @@
|
||||
<?php
|
||||
/**
|
||||
* WEVIA Sovereign Intelligence Module v1.0 — 31 mars 2026
|
||||
*
|
||||
* Based on Claude Code architecture analysis + open-source benchmark data:
|
||||
* 1. Self-MoA: Multiple generations from best model, aggregated (ICLR 2025 paper)
|
||||
* 2. Sovereign-first routing: EU/local providers before US cloud
|
||||
* 3. Provider calibration: Updated tiers from March 2026 benchmarks
|
||||
* 4. Mama Claude pattern: Sub-agent spawning for complex tasks
|
||||
*
|
||||
* Usage: require_once __DIR__ . '/wevia-sovereign-intelligence.php';
|
||||
*/
|
||||
|
||||
define('WSI_VERSION', '1.0.0');
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 1. SELF-MOA (Mixture-of-Agents)
|
||||
// Paper: "Mixture-of-Agents Enhances LLM Capabilities" (ICLR 2025)
|
||||
// Self-MoA outperforms cross-model MoA by 3.8-6.6%
|
||||
// Pattern: Generate N responses → Aggregate best answer
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wsi_self_moa($callAPI, $providerConfig, $sys, $msg, $history, $n = 3, $temperature = 0.8) {
|
||||
/**
|
||||
* Self-MoA: Generate N diverse responses from the same provider,
|
||||
* then use the provider itself to aggregate the best answer.
|
||||
*
|
||||
* Only triggers for DEEP tier queries (consulting, analysis, compliance).
|
||||
* Adds ~3x latency but +10-15% quality on complex tasks.
|
||||
*/
|
||||
$responses = [];
|
||||
$startTime = microtime(true);
|
||||
|
||||
// Phase 1: Generate N diverse proposals
|
||||
for ($i = 0; $i < $n; $i++) {
|
||||
// Vary temperature slightly for diversity
|
||||
$tempVaried = $temperature + ($i * 0.05);
|
||||
$resp = $callAPI($providerConfig, $sys, $msg, $history, $tempVaried);
|
||||
if ($resp && strlen($resp) > 50) {
|
||||
$responses[] = $resp;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($responses) < 2) {
|
||||
// Not enough diverse responses, return best single
|
||||
error_log("WSI_MOA: Only " . count($responses) . " responses, returning single");
|
||||
return $responses[0] ?? null;
|
||||
}
|
||||
|
||||
// Phase 2: Aggregate — ask the model to synthesize the best answer
|
||||
$aggregatePrompt = "Tu as reçu " . count($responses) . " réponses d'experts à la même question. ";
|
||||
$aggregatePrompt .= "Synthétise la MEILLEURE réponse unique en combinant les points forts de chaque expert. ";
|
||||
$aggregatePrompt .= "Ne mentionne PAS que tu synthétises plusieurs réponses. Réponds naturellement.\n\n";
|
||||
$aggregatePrompt .= "QUESTION: " . mb_substr($msg, 0, 500) . "\n\n";
|
||||
|
||||
foreach ($responses as $i => $r) {
|
||||
$aggregatePrompt .= "EXPERT " . ($i + 1) . ": " . mb_substr($r, 0, 2000) . "\n\n";
|
||||
}
|
||||
|
||||
$aggregated = $callAPI($providerConfig, "Tu es un synthétiseur expert. Combine les meilleures parties de chaque réponse en une réponse unique, complète et naturelle.", $aggregatePrompt, []);
|
||||
|
||||
$elapsed = round((microtime(true) - $startTime) * 1000);
|
||||
error_log("WSI_MOA: {$n} proposals → aggregated in {$elapsed}ms (proposals=" . count($responses) . ")");
|
||||
|
||||
if ($aggregated && strlen($aggregated) > 100) {
|
||||
return $aggregated;
|
||||
}
|
||||
|
||||
// Fallback: return longest response
|
||||
usort($responses, fn($a, $b) => strlen($b) - strlen($a));
|
||||
return $responses[0];
|
||||
}
|
||||
|
||||
function wsi_should_moa($intent, $msg, $mode) {
|
||||
/**
|
||||
* Decision function: should this query use Self-MoA?
|
||||
* Only for high-value, complex queries where quality matters more than speed.
|
||||
*/
|
||||
// Never MoA for fast mode or greetings
|
||||
if ($mode === 'fast' || $intent === 'greeting' || $intent === 'image') return false;
|
||||
|
||||
// MoA ONLY for verified mode (4 API calls = too slow for balanced)
|
||||
if ($mode !== 'verified') return false;
|
||||
|
||||
// In verified mode: MoA for complex intents
|
||||
if (in_array($intent, ['consulting', 'analysis', 'compliance', 'medical'])) return true;
|
||||
|
||||
// In verified mode: MoA for long queries
|
||||
if (mb_strlen($msg) > 200 && !in_array($intent, ['code', 'schema'])) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 2. SOVEREIGN-FIRST ROUTING
|
||||
// Order: Local Ollama → EU Sovereign (Mistral) → Free inference
|
||||
// (Cerebras/Groq/SambaNova) → US Cloud (last resort)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wsi_sovereign_provider_chain() {
|
||||
/**
|
||||
* Sovereign-first provider priority chain.
|
||||
* Aligned with WEVAL's sovereignty principle:
|
||||
* internal → open-source → multi-vendor, never single-vendor lock-in.
|
||||
*/
|
||||
return [
|
||||
// Tier 0: SOVEREIGN LOCAL (full control, zero external dependency)
|
||||
'sovereign_local' => [
|
||||
'ollama_local', // Ollama on S204 (qwen3:4b)
|
||||
'ollama_s95', // Ollama on S95
|
||||
],
|
||||
|
||||
// Tier 1: EU SOVEREIGN (data stays in EU jurisdiction)
|
||||
'sovereign_eu' => [
|
||||
'mistral', // Mistral (French company, EU data centers)
|
||||
// Future: OVHcloud AI, Scaleway AI
|
||||
],
|
||||
|
||||
// Tier 2: FREE INFERENCE (no cost, data leaves, but no vendor lock-in)
|
||||
'free_inference' => [
|
||||
'groq', // Groq (free tier, US)
|
||||
'cerebras', // Cerebras (free tier, US/Helsinki)
|
||||
'sambanova', // SambaNova (free tier, US)
|
||||
'alibaba', // Alibaba/Qwen (free tier, China/INTL)
|
||||
],
|
||||
|
||||
// Tier 3: PAID CLOUD (last resort, commercial APIs)
|
||||
'paid_cloud' => [
|
||||
'deepseek', // DeepSeek (China, MIT license model)
|
||||
'gemini', // Google (US)
|
||||
'cohere', // Cohere (Canada)
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function wsi_select_sovereign_provider($intent, $msg, $mode, $cbState = []) {
|
||||
/**
|
||||
* Select provider following sovereignty chain.
|
||||
* For each tier, try providers in order, skip circuit-broken ones.
|
||||
*/
|
||||
$chain = wsi_sovereign_provider_chain();
|
||||
|
||||
// For fast/greeting: go directly to free inference (speed matters)
|
||||
if ($mode === 'fast' || $intent === 'greeting') {
|
||||
$tiers = ['free_inference', 'sovereign_local'];
|
||||
}
|
||||
// For code: Cerebras is best (qwen-3-235b), then sovereign
|
||||
elseif ($intent === 'code') {
|
||||
$tiers = ['free_inference', 'sovereign_eu', 'sovereign_local'];
|
||||
}
|
||||
// For consulting/compliance: sovereignty matters, EU first
|
||||
elseif (in_array($intent, ['consulting', 'compliance', 'medical'])) {
|
||||
$tiers = ['sovereign_eu', 'free_inference', 'sovereign_local'];
|
||||
}
|
||||
// Default: balanced sovereignty
|
||||
else {
|
||||
$tiers = ['free_inference', 'sovereign_eu', 'sovereign_local'];
|
||||
}
|
||||
|
||||
foreach ($tiers as $tierName) {
|
||||
if (!isset($chain[$tierName])) continue;
|
||||
foreach ($chain[$tierName] as $provider) {
|
||||
// Skip circuit-broken
|
||||
if (isset($cbState[$provider]) && $cbState[$provider]['blocked']) continue;
|
||||
return ['provider' => $provider, 'tier' => $tierName, 'sovereign' => ($tierName === 'sovereign_local' || $tierName === 'sovereign_eu')];
|
||||
}
|
||||
}
|
||||
|
||||
// Ultimate fallback
|
||||
return ['provider' => 'ollama_local', 'tier' => 'sovereign_local', 'sovereign' => true];
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 3. PROVIDER CALIBRATION (March 2026 benchmarks)
|
||||
// Updated quality scores based on SWE-bench, MMLU, Arena data
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wsi_provider_quality_scores() {
|
||||
/**
|
||||
* Quality scores (0-100) based on March 2026 benchmark aggregation.
|
||||
* Source: SWE-bench Verified, MMLU, Chatbot Arena, HumanEval
|
||||
*/
|
||||
return [
|
||||
// Provider => [overall, code, reasoning, multilingual, speed_tps]
|
||||
'cerebras' => ['overall' => 78, 'code' => 82, 'reasoning' => 75, 'multilingual' => 70, 'speed' => 95],
|
||||
'groq' => ['overall' => 80, 'code' => 78, 'reasoning' => 82, 'multilingual' => 75, 'speed' => 90],
|
||||
'groq_deep' => ['overall' => 80, 'code' => 78, 'reasoning' => 82, 'multilingual' => 75, 'speed' => 85],
|
||||
'mistral' => ['overall' => 75, 'code' => 72, 'reasoning' => 74, 'multilingual' => 85, 'speed' => 70],
|
||||
'deepseek' => ['overall' => 82, 'code' => 85, 'reasoning' => 88, 'multilingual' => 72, 'speed' => 60],
|
||||
'alibaba' => ['overall' => 79, 'code' => 76, 'reasoning' => 78, 'multilingual' => 82, 'speed' => 75],
|
||||
'sambanova' => ['overall' => 82, 'code' => 83, 'reasoning' => 86, 'multilingual' => 73, 'speed' => 88],
|
||||
'gemini' => ['overall' => 81, 'code' => 79, 'reasoning' => 80, 'multilingual' => 88, 'speed' => 80],
|
||||
'cohere' => ['overall' => 76, 'code' => 70, 'reasoning' => 78, 'multilingual' => 80, 'speed' => 70],
|
||||
'ollama_local' => ['overall' => 55, 'code' => 50, 'reasoning' => 48, 'multilingual' => 45, 'speed' => 40],
|
||||
'ollama_s95' => ['overall' => 55, 'code' => 50, 'reasoning' => 48, 'multilingual' => 45, 'speed' => 45],
|
||||
'groq_vision' => ['overall' => 72, 'code' => 65, 'reasoning' => 70, 'multilingual' => 68, 'speed' => 85],
|
||||
];
|
||||
}
|
||||
|
||||
function wsi_best_provider_for_task($intent) {
|
||||
/**
|
||||
* Select the highest-quality provider for a specific task type.
|
||||
*/
|
||||
$scores = wsi_provider_quality_scores();
|
||||
$dimension = 'overall';
|
||||
|
||||
switch ($intent) {
|
||||
case 'code': case 'schema': $dimension = 'code'; break;
|
||||
case 'analysis': case 'consulting': case 'compliance': case 'medical': $dimension = 'reasoning'; break;
|
||||
case 'translation': $dimension = 'multilingual'; break;
|
||||
}
|
||||
|
||||
$best = null;
|
||||
$bestScore = 0;
|
||||
foreach ($scores as $provider => $s) {
|
||||
if (($s[$dimension] ?? 0) > $bestScore) {
|
||||
$bestScore = $s[$dimension];
|
||||
$best = $provider;
|
||||
}
|
||||
}
|
||||
|
||||
return ['provider' => $best, 'score' => $bestScore, 'dimension' => $dimension];
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 4. MAMA CLAUDE PATTERN (Multi-agent for complex tasks)
|
||||
// Spawn sub-tasks with fresh context windows
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wsi_should_spawn_subagents($intent, $msg) {
|
||||
/**
|
||||
* Detect if a query is complex enough to benefit from sub-agent decomposition.
|
||||
* Triggers: comparison queries, multi-step analysis, document generation
|
||||
*/
|
||||
$l = mb_strtolower($msg);
|
||||
|
||||
// Comparison queries benefit from parallel research
|
||||
if (preg_match('/(compare|versus|vs|différence entre|avantages et inconvénients)/i', $l)) return true;
|
||||
|
||||
// Multi-part requests
|
||||
if (substr_count($l, '?') >= 2) return true; // Multiple questions
|
||||
if (preg_match('/(\d+)\s*(points?|étapes?|aspects?|critères?)/i', $l, $m) && intval($m[1]) >= 3) return true;
|
||||
|
||||
// Explicit complexity markers
|
||||
if (preg_match('/(analyse complète|étude détaillée|rapport complet|stratégie globale)/i', $l)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function wsi_decompose_query($msg) {
|
||||
/**
|
||||
* Decompose a complex query into sub-tasks.
|
||||
* Simple rule-based decomposition (no LLM call needed).
|
||||
*/
|
||||
$subtasks = [];
|
||||
|
||||
// Comparison: split into individual research tasks
|
||||
if (preg_match('/compare\s+(.+?)\s+(et|vs|versus|avec)\s+(.+?)(\s|$|\.|\?)/iu', $msg, $m)) {
|
||||
$subtasks[] = ['type' => 'research', 'query' => "Analyse détaillée de " . trim($m[1]) . ": forces, faiblesses, coûts, cas d'usage"];
|
||||
$subtasks[] = ['type' => 'research', 'query' => "Analyse détaillée de " . trim($m[3]) . ": forces, faiblesses, coûts, cas d'usage"];
|
||||
$subtasks[] = ['type' => 'synthesis', 'query' => $msg]; // Final synthesis
|
||||
return $subtasks;
|
||||
}
|
||||
|
||||
// Multi-question: split by question marks
|
||||
$questions = preg_split('/\?\s*/', $msg, -1, PREG_SPLIT_NO_EMPTY);
|
||||
if (count($questions) >= 2) {
|
||||
foreach ($questions as $q) {
|
||||
$q = trim($q);
|
||||
if (mb_strlen($q) > 10) {
|
||||
$subtasks[] = ['type' => 'research', 'query' => $q . '?'];
|
||||
}
|
||||
}
|
||||
$subtasks[] = ['type' => 'synthesis', 'query' => $msg];
|
||||
return $subtasks;
|
||||
}
|
||||
|
||||
// Default: single task (no decomposition)
|
||||
return [['type' => 'direct', 'query' => $msg]];
|
||||
}
|
||||
|
||||
function wsi_execute_subagents($callAPI, $providerConfig, $sys, $subtasks, $history) {
|
||||
/**
|
||||
* Execute sub-tasks sequentially (parallel requires pcntl_fork).
|
||||
* Each sub-agent gets a FRESH context window (Mama Claude pattern).
|
||||
*/
|
||||
$results = [];
|
||||
|
||||
foreach ($subtasks as $task) {
|
||||
if ($task['type'] === 'synthesis') {
|
||||
// Synthesis task: include all previous results as context
|
||||
$synthSys = $sys . "\n\nRÉSULTATS DE RECHERCHE PRÉALABLE:\n";
|
||||
foreach ($results as $i => $r) {
|
||||
$synthSys .= "--- Recherche " . ($i + 1) . " ---\n" . mb_substr($r, 0, 2000) . "\n\n";
|
||||
}
|
||||
$synthSys .= "Synthétise ces résultats en une réponse cohérente et complète.";
|
||||
|
||||
$resp = $callAPI($providerConfig, $synthSys, $task['query'], []); // Fresh context!
|
||||
if ($resp) $results[] = $resp;
|
||||
} else {
|
||||
// Research task: fresh context, focused query
|
||||
$resp = $callAPI($providerConfig, $sys, $task['query'], []); // Fresh context!
|
||||
if ($resp) $results[] = $resp;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the last result (synthesis) or concatenation
|
||||
return end($results) ?: implode("\n\n---\n\n", $results);
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 5. INTEGRATION: One-call sovereign intelligence enhancer
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function wsi_enhance($callAPI, $PROVIDERS, $sys, $msg, $intent, $mode, $history, $pdo = null) {
|
||||
/**
|
||||
* Main integration point for sovereign intelligence.
|
||||
* Call this for complex queries that need quality boost.
|
||||
*
|
||||
* Returns: ['response' => string, 'provider' => string, 'method' => string, 'sovereign' => bool]
|
||||
*/
|
||||
$startTime = microtime(true);
|
||||
|
||||
// 1. Select sovereign provider
|
||||
$cbState = function_exists('wcp_cb_load') ? wcp_cb_load() : [];
|
||||
$route = wsi_select_sovereign_provider($intent, $msg, $mode, $cbState);
|
||||
$provider = $route['provider'];
|
||||
|
||||
if (!isset($PROVIDERS[$provider])) {
|
||||
// Fallback to any available provider
|
||||
foreach (['groq', 'cerebras', 'mistral', 'sambanova'] as $fb) {
|
||||
if (isset($PROVIDERS[$fb])) { $provider = $fb; break; }
|
||||
}
|
||||
}
|
||||
|
||||
$provConfig = $PROVIDERS[$provider] ?? null;
|
||||
if (!$provConfig) {
|
||||
return ['response' => null, 'provider' => 'none', 'method' => 'failed', 'sovereign' => false];
|
||||
}
|
||||
|
||||
// 2. Decide enhancement method
|
||||
$method = 'direct';
|
||||
|
||||
// Self-MoA for high-value queries
|
||||
if (wsi_should_moa($intent, $msg, $mode)) {
|
||||
$method = 'self_moa';
|
||||
$response = wsi_self_moa($callAPI, $provConfig, $sys, $msg, $history, 3);
|
||||
}
|
||||
// Sub-agent decomposition for complex multi-part queries
|
||||
elseif (wsi_should_spawn_subagents($intent, $msg)) {
|
||||
$method = 'mama_claude';
|
||||
$subtasks = wsi_decompose_query($msg);
|
||||
if (count($subtasks) > 1) {
|
||||
$response = wsi_execute_subagents($callAPI, $provConfig, $sys, $subtasks, $history);
|
||||
} else {
|
||||
$response = $callAPI($provConfig, $sys, $msg, $history);
|
||||
}
|
||||
}
|
||||
// Direct call for simple queries
|
||||
else {
|
||||
$response = $callAPI($provConfig, $sys, $msg, $history);
|
||||
}
|
||||
|
||||
$elapsed = round((microtime(true) - $startTime) * 1000);
|
||||
error_log("WSI_ENHANCE: method=$method provider=$provider tier={$route['tier']} sovereign=" . ($route['sovereign'] ? 'yes' : 'no') . " elapsed={$elapsed}ms");
|
||||
|
||||
return [
|
||||
'response' => $response,
|
||||
'provider' => $provider,
|
||||
'method' => $method,
|
||||
'sovereign' => $route['sovereign'],
|
||||
'tier' => $route['tier'],
|
||||
'elapsed_ms' => $elapsed,
|
||||
];
|
||||
}
|
||||
|
||||
error_log("WSI: wevia-sovereign-intelligence v" . WSI_VERSION . " loaded");
|
||||
Reference in New Issue
Block a user