271 lines
14 KiB
PHP
Executable File
271 lines
14 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* WEVIA OPUS — Tool Use Engine v2
|
|
* 15 outils intégrés avec validation, retry, et logging
|
|
*/
|
|
|
|
class ToolRegistry {
|
|
|
|
private static array $tools = [
|
|
'web_search' => [
|
|
'description' => 'Recherche sur le web via SearXNG pour des informations récentes ou vérification de faits.',
|
|
'parameters' => ['query' => 'string (la recherche)', 'max_results' => 'int (défaut: 5)'],
|
|
'example' => '<tool_call>{"tool":"web_search","params":{"query":"PowerMTA latest version 2025","max_results":3}}</tool_call>',
|
|
'requires' => ['network'],
|
|
'timeout' => 10
|
|
],
|
|
'execute_python' => [
|
|
'description' => 'Exécute du code Python pour calculs, traitement de données, visualisation. Bibliothèques disponibles: numpy, pandas, matplotlib, requests.',
|
|
'parameters' => ['code' => 'string (code Python)'],
|
|
'example' => '<tool_call>{"tool":"execute_python","params":{"code":"import pandas as pd\ndf = pd.read_csv(\'/tmp/data.csv\')\nprint(df.describe())"}}</tool_call>',
|
|
'requires' => ['python3'],
|
|
'timeout' => 30,
|
|
'sandbox' => true
|
|
],
|
|
'execute_bash' => [
|
|
'description' => 'Exécute une commande bash pour administration système, fichiers, réseau.',
|
|
'parameters' => ['command' => 'string (commande bash)'],
|
|
'example' => '<tool_call>{"tool":"execute_bash","params":{"command":"df -h && free -m && uptime"}}</tool_call>',
|
|
'requires' => ['bash'],
|
|
'timeout' => 15,
|
|
'dangerous_patterns' => ['rm -rf /', 'mkfs', 'dd if=/dev/zero', '> /dev/sda', 'chmod 777 /']
|
|
],
|
|
'database_query' => [
|
|
'description' => 'Exécute une requête PostgreSQL SELECT (lecture seule) sur les bases WEVADS ou WEVIA.',
|
|
'parameters' => ['query' => 'string (SQL SELECT)', 'database' => 'string (adx_system|adx_clients|wevia_db)'],
|
|
'example' => '<tool_call>{"tool":"database_query","params":{"query":"SELECT COUNT(*) FROM admin.warmup_accounts WHERE status=\'active\'","database":"adx_system"}}</tool_call>',
|
|
'requires' => ['postgresql'],
|
|
'timeout' => 10,
|
|
'read_only' => true
|
|
],
|
|
'file_read' => [
|
|
'description' => 'Lit le contenu d\'un fichier texte.',
|
|
'parameters' => ['path' => 'string (chemin absolu)', 'lines' => 'int (nombre de lignes, défaut: toutes)'],
|
|
'example' => '<tool_call>{"tool":"file_read","params":{"path":"/etc/pmta/config","lines":50}}</tool_call>',
|
|
'requires' => [],
|
|
'timeout' => 5,
|
|
'blocked_paths' => ['/etc/shadow', '/etc/passwd', '*.key', '*.pem']
|
|
],
|
|
'file_write' => [
|
|
'description' => 'Écrit du contenu dans un fichier (avec backup automatique).',
|
|
'parameters' => ['path' => 'string', 'content' => 'string', 'backup' => 'bool (défaut: true)'],
|
|
'example' => '<tool_call>{"tool":"file_write","params":{"path":"/tmp/test.txt","content":"Hello World"}}</tool_call>',
|
|
'requires' => [],
|
|
'timeout' => 5
|
|
],
|
|
'http_request' => [
|
|
'description' => 'Fait une requête HTTP GET/POST vers une URL.',
|
|
'parameters' => ['url' => 'string', 'method' => 'string (GET|POST)', 'headers' => 'object', 'body' => 'string'],
|
|
'example' => '<tool_call>{"tool":"http_request","params":{"url":"http://127.0.0.1:5821/api/status","method":"GET"}}</tool_call>',
|
|
'requires' => ['network'],
|
|
'timeout' => 15
|
|
],
|
|
'ollama_generate' => [
|
|
'description' => 'Génère du texte avec un modèle Ollama spécifique (utile pour comparer modèles ou utiliser un spécialiste).',
|
|
'parameters' => ['model' => 'string', 'prompt' => 'string', 'system' => 'string'],
|
|
'example' => '<tool_call>{"tool":"ollama_generate","params":{"model":"deepseek-r1:32b","prompt":"Explique les CTE récursifs en SQL","system":"Tu es un expert PostgreSQL."}}</tool_call>',
|
|
'requires' => ['ollama'],
|
|
'timeout' => 60
|
|
],
|
|
'embedding_generate' => [
|
|
'description' => 'Génère un embedding vectoriel pour du texte (768 dimensions, nomic-embed-text).',
|
|
'parameters' => ['text' => 'string'],
|
|
'example' => '<tool_call>{"tool":"embedding_generate","params":{"text":"Comment configurer DKIM dans PowerMTA?"}}</tool_call>',
|
|
'requires' => ['ollama'],
|
|
'timeout' => 10
|
|
],
|
|
'rag_search' => [
|
|
'description' => 'Recherche sémantique dans la base de connaissances WEVIA (pgvector).',
|
|
'parameters' => ['query' => 'string', 'top_k' => 'int (défaut: 5)', 'threshold' => 'float (défaut: 0.5)'],
|
|
'example' => '<tool_call>{"tool":"rag_search","params":{"query":"warming schedule IP PowerMTA","top_k":3}}</tool_call>',
|
|
'requires' => ['postgresql', 'ollama'],
|
|
'timeout' => 15
|
|
],
|
|
'calculator' => [
|
|
'description' => 'Calcul mathématique précis (évite les erreurs d\'arrondi des LLMs).',
|
|
'parameters' => ['expression' => 'string (expression mathématique PHP)'],
|
|
'example' => '<tool_call>{"tool":"calculator","params":{"expression":"1783 * 43.2 / 365"}}</tool_call>',
|
|
'requires' => [],
|
|
'timeout' => 2
|
|
],
|
|
'system_info' => [
|
|
'description' => 'Informations système du serveur (CPU, RAM, disk, GPU, uptime, services).',
|
|
'parameters' => ['server' => 'string (s88|s89|s46|s151)'],
|
|
'example' => '<tool_call>{"tool":"system_info","params":{"server":"s88"}}</tool_call>',
|
|
'requires' => ['bash'],
|
|
'timeout' => 10
|
|
],
|
|
'dns_lookup' => [
|
|
'description' => 'Requête DNS pour vérifier SPF, DKIM, DMARC, MX, A records.',
|
|
'parameters' => ['domain' => 'string', 'type' => 'string (A|MX|TXT|CNAME|NS|AAAA)'],
|
|
'example' => '<tool_call>{"tool":"dns_lookup","params":{"domain":"weval-consulting.com","type":"TXT"}}</tool_call>',
|
|
'requires' => ['network'],
|
|
'timeout' => 5
|
|
],
|
|
'json_transform' => [
|
|
'description' => 'Transforme, filtre, agrège des données JSON.',
|
|
'parameters' => ['data' => 'array|string', 'operation' => 'string (filter|sort|group|count|sum|unique)'],
|
|
'example' => '<tool_call>{"tool":"json_transform","params":{"data":[1,2,3,4,5],"operation":"sum"}}</tool_call>',
|
|
'requires' => [],
|
|
'timeout' => 5
|
|
],
|
|
'sentinel_exec' => [
|
|
'description' => 'Exécute une commande sur un serveur distant via l\'API Sentinel.',
|
|
'parameters' => ['target' => 'string (s88|s46|s151)', 'command' => 'string'],
|
|
'example' => '<tool_call>{"tool":"sentinel_exec","params":{"target":"s88","command":"nvidia-smi --query-gpu=memory.used,memory.total --format=csv"}}</tool_call>',
|
|
'requires' => ['network', 'sentinel'],
|
|
'timeout' => 30
|
|
]
|
|
];
|
|
|
|
/**
|
|
* Génère le prompt décrivant tous les outils disponibles
|
|
*/
|
|
public static function getToolsPrompt(): string {
|
|
$prompt = "## Outils disponibles\n\n";
|
|
$prompt .= "Tu peux utiliser les outils suivants en insérant un bloc <tool_call> dans ta réponse.\n";
|
|
$prompt .= "Format: <tool_call>{\"tool\":\"nom\",\"params\":{...}}</tool_call>\n\n";
|
|
|
|
foreach (self::$tools as $name => $tool) {
|
|
$prompt .= "### $name\n";
|
|
$prompt .= $tool['description'] . "\n";
|
|
$prompt .= "Paramètres: " . json_encode($tool['parameters'], JSON_UNESCAPED_UNICODE) . "\n";
|
|
$prompt .= "Exemple: " . $tool['example'] . "\n\n";
|
|
}
|
|
|
|
$prompt .= "### Règles d'utilisation des outils\n";
|
|
$prompt .= "1. N'utilise un outil QUE si c'est nécessaire pour répondre correctement\n";
|
|
$prompt .= "2. Préfère tes connaissances internes pour les faits généraux\n";
|
|
$prompt .= "3. Utilise database_query pour les stats WEVADS en temps réel\n";
|
|
$prompt .= "4. Utilise rag_search pour les questions sur WEVAL, WEVIA, SAP, email marketing\n";
|
|
$prompt .= "5. JAMAIS de requêtes destructives (DELETE, DROP, UPDATE sans WHERE)\n";
|
|
$prompt .= "6. Toujours vérifier les résultats avant de les présenter\n";
|
|
|
|
return $prompt;
|
|
}
|
|
|
|
/**
|
|
* Parse et exécute les tool calls dans une réponse LLM
|
|
*/
|
|
public static function processResponse(string $response): array {
|
|
$results = [];
|
|
|
|
if (preg_match_all('/<tool_call>(.*?)<\/tool_call>/s', $response, $matches)) {
|
|
foreach ($matches[1] as $call_json) {
|
|
$call = json_decode(trim($call_json), true);
|
|
if (!$call || !isset($call['tool'])) continue;
|
|
|
|
$tool_name = $call['tool'];
|
|
$params = $call['params'] ?? [];
|
|
|
|
if (!isset(self::$tools[$tool_name])) {
|
|
$results[] = ['tool' => $tool_name, 'error' => "Outil inconnu: $tool_name"];
|
|
continue;
|
|
}
|
|
|
|
$tool_config = self::$tools[$tool_name];
|
|
|
|
// Vérification sécurité pour bash
|
|
if ($tool_name === 'execute_bash' && isset($params['command'])) {
|
|
foreach ($tool_config['dangerous_patterns'] ?? [] as $pattern) {
|
|
if (stripos($params['command'], $pattern) !== false) {
|
|
$results[] = ['tool' => $tool_name, 'error' => "BLOQUÉ: Commande dangereuse détectée ($pattern)"];
|
|
continue 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Vérification read-only pour database
|
|
if ($tool_name === 'database_query' && isset($params['query'])) {
|
|
$q_upper = strtoupper(trim($params['query']));
|
|
if (!str_starts_with($q_upper, 'SELECT') && !str_starts_with($q_upper, 'WITH') && !str_starts_with($q_upper, 'EXPLAIN')) {
|
|
$results[] = ['tool' => $tool_name, 'error' => "BLOQUÉ: Seules les requêtes SELECT/WITH/EXPLAIN sont autorisées"];
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Exécuter l'outil
|
|
try {
|
|
$start = microtime(true);
|
|
$result = self::executeTool($tool_name, $params, $tool_config);
|
|
$duration = microtime(true) - $start;
|
|
|
|
$results[] = [
|
|
'tool' => $tool_name,
|
|
'result' => $result,
|
|
'duration_ms' => round($duration * 1000),
|
|
'success' => true
|
|
];
|
|
} catch (\Exception $e) {
|
|
$results[] = [
|
|
'tool' => $tool_name,
|
|
'error' => $e->getMessage(),
|
|
'success' => false
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
private static function executeTool(string $name, array $params, array $config): mixed {
|
|
$timeout = $config['timeout'] ?? 10;
|
|
|
|
switch ($name) {
|
|
case 'calculator':
|
|
$expr = preg_replace('/[^0-9+\-*\/().,%\s]/', '', $params['expression'] ?? '0');
|
|
return eval("return $expr;");
|
|
|
|
case 'system_info':
|
|
$server = $params['server'] ?? 's88';
|
|
$cmd = "echo '=== SYSTEM ===' && uname -a && echo '=== CPU ===' && nproc && echo '=== RAM ===' && free -m | head -2 && echo '=== DISK ===' && df -h / | tail -1 && echo '=== UPTIME ===' && uptime";
|
|
return shell_exec($cmd);
|
|
|
|
case 'file_read':
|
|
$path = $params['path'] ?? '';
|
|
if (!file_exists($path)) return "Fichier non trouvé: $path";
|
|
$lines = $params['lines'] ?? null;
|
|
if ($lines) {
|
|
return implode("\n", array_slice(file($path), 0, $lines));
|
|
}
|
|
return file_get_contents($path);
|
|
|
|
case 'dns_lookup':
|
|
$domain = escapeshellarg($params['domain'] ?? '');
|
|
$type = escapeshellarg($params['type'] ?? 'A');
|
|
return shell_exec("dig $type $domain +short 2>&1");
|
|
|
|
case 'http_request':
|
|
$url = $params['url'] ?? '';
|
|
$method = strtoupper($params['method'] ?? 'GET');
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
|
if ($method === 'POST') {
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $params['body'] ?? '');
|
|
}
|
|
$result = curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
return ['status' => $code, 'body' => substr($result, 0, 5000)];
|
|
|
|
case 'json_transform':
|
|
$data = $params['data'] ?? [];
|
|
$op = $params['operation'] ?? 'count';
|
|
if (is_string($data)) $data = json_decode($data, true);
|
|
return match($op) {
|
|
'count' => count($data),
|
|
'sum' => array_sum($data),
|
|
'unique' => array_values(array_unique($data)),
|
|
'sort' => (function() use ($data) { sort($data); return $data; })(),
|
|
default => $data
|
|
};
|
|
|
|
default:
|
|
return "Tool handler not implemented for: $name (placeholder — wire to real backend)";
|
|
}
|
|
}
|
|
}
|