provider = aiProvider(); $this->model = aiModel(); $this->apiKey = $_ENV['ANTHROPIC_API_KEY'] ?? getenv('ANTHROPIC_API_KEY') ?: ''; } public function isConfigured(): bool { if ($this->provider === 'claude_code') { return is_executable('/usr/local/bin/claude'); } return $this->apiKey !== ''; } /** @return array{ok: bool, text?: string, error?: string} */ public function query(string $action, string $title, string $content): array { $content = mb_substr(trim($content), 0, 8000); if ($content === '') { return ['ok' => false, 'error' => "Contenu de l'article vide"]; } $userMsg = $title !== '' ? "# {$title}\n\n{$content}" : $content; if ($action === 'analyze') { $raw = $this->provider === 'claude_code' ? $this->queryClaudeCode(self::SYSTEM_ANALYZE, $userMsg) : $this->queryAnthropicRaw(self::SYSTEM_ANALYZE, $userMsg, 4096); if (!$raw['ok']) { return $raw; } return $this->parseAnalyzeResponse($raw['text'] ?? ''); } $systemPrompt = match ($action) { 'critique' => self::SYSTEM_CRITIQUE, 'rewrite' => self::SYSTEM_REWRITE, default => null, }; if ($systemPrompt === null) { return ['ok' => false, 'error' => 'Action inconnue']; } if ($this->provider === 'claude_code') { return $this->queryClaudeCode($systemPrompt, $userMsg); } return $this->queryAnthropic($action, $systemPrompt, $userMsg); } /** @return array{ok: bool, critique?: string, rewrite?: string, error?: string} */ private function parseAnalyzeResponse(string $text): array { $parts = preg_split('/===CRITIQUE===|===REWRITE===/', $text); if (count($parts) < 3) { // Fallback : pas de séparateurs trouvés, on met tout en critique return ['ok' => true, 'critique' => trim($text), 'rewrite' => '']; } return [ 'ok' => true, 'critique' => trim($parts[1]), 'rewrite' => trim($parts[2]), ]; } /** @return array{ok: bool, text?: string, error?: string} */ private function queryAnthropicRaw(string $systemPrompt, string $userMsg, int $maxTokens): array { if ($this->apiKey === '') { return ['ok' => false, 'error' => 'Clé Anthropic non configurée (ANTHROPIC_API_KEY manquante dans .env)']; } $payload = json_encode([ 'model' => $this->model, 'max_tokens' => $maxTokens, 'system' => $systemPrompt, 'messages' => [['role' => 'user', 'content' => $userMsg]], ]); $ch = curl_init('https://api.anthropic.com/v1/messages'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_TIMEOUT => 90, CURLOPT_HTTPHEADER => [ 'x-api-key: ' . $this->apiKey, 'anthropic-version: 2023-06-01', 'Content-Type: application/json', ], ]); $resp = curl_exec($ch); $http = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if ($err !== '') { return ['ok' => false, 'error' => 'Erreur réseau : ' . $err]; } $data = json_decode((string) $resp, true); if ($http !== 200) { return ['ok' => false, 'error' => $data['error']['message'] ?? ('Anthropic HTTP ' . $http)]; } return ['ok' => true, 'text' => $data['content'][0]['text'] ?? '']; } /** @return array{ok: bool, text?: string, error?: string} */ private function queryAnthropic(string $action, string $systemPrompt, string $userMsg): array { if ($this->apiKey === '') { return ['ok' => false, 'error' => 'Clé Anthropic non configurée (ANTHROPIC_API_KEY manquante dans .env)']; } $maxTokens = ($action === 'rewrite') ? 4096 : 1200; $payload = json_encode([ 'model' => $this->model, 'max_tokens' => $maxTokens, 'system' => $systemPrompt, 'messages' => [['role' => 'user', 'content' => $userMsg]], ]); $ch = curl_init('https://api.anthropic.com/v1/messages'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_TIMEOUT => 60, CURLOPT_HTTPHEADER => [ 'x-api-key: ' . $this->apiKey, 'anthropic-version: 2023-06-01', 'Content-Type: application/json', ], ]); $resp = curl_exec($ch); $http = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if ($err !== '') { return ['ok' => false, 'error' => 'Erreur réseau : ' . $err]; } $data = json_decode((string) $resp, true); if ($http !== 200) { $msg = $data['error']['message'] ?? ('Anthropic HTTP ' . $http); return ['ok' => false, 'error' => $msg]; } $text = $data['content'][0]['text'] ?? ''; return ['ok' => true, 'text' => $text]; } /** @return array{ok: bool, text?: string, error?: string} */ private function queryClaudeCode(string $systemPrompt, string $userMsg): array { $bin = '/usr/local/bin/claude'; if (!is_executable($bin)) { return ['ok' => false, 'error' => 'Claude Code CLI introuvable (/usr/local/bin/claude)']; } $prompt = $systemPrompt . "\n\n" . $userMsg; $cmd = $bin . ' --print ' . escapeshellarg($prompt) . ' 2>&1'; $env = ['HOME' => '/var/lib/claude-www', 'PATH' => '/usr/local/bin:/usr/bin:/bin']; $desc = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; $proc = proc_open($cmd, $desc, $pipes, '/tmp', $env); if (!is_resource($proc)) { return ['ok' => false, 'error' => 'proc_open échoué']; } fclose($pipes[0]); $out = stream_get_contents($pipes[1]); fclose($pipes[1]); fclose($pipes[2]); $code = proc_close($proc); if ($code !== 0) { return ['ok' => false, 'error' => 'Claude Code exit ' . $code . ' : ' . trim((string)$out)]; } return ['ok' => true, 'text' => trim((string)$out)]; } }