apiKey = env('GROQ_API_KEY'); // Nếu có model trong ENV, đưa nó lên đầu danh sách ưu tiên $envModel = env('GROQ_MODEL'); if ($envModel && !in_array($envModel, $this->availableModels)) { array_unshift($this->availableModels, $envModel); } } /** * Chuyển đổi FEN thành bản đồ trực quan (ASCII Map) để AI "nhìn" thấy bàn cờ. */ private function fenToVisualBoard($fen) { $parts = explode(' ', $fen); $rows = explode('/', $parts[0]); $visual = "\n--- VISUAL BOARD (Góc nhìn bên Đỏ) ---\n"; $visual .= "Cột: 1 2 3 4 5 6 7 8 9 (Của Đen)\n"; $visual .= " 9 8 7 6 5 4 3 2 1 (Của Đỏ)\n"; $visual .= " -------------------\n"; foreach ($rows as $index => $row) { $line = ""; // Expand numbers to dots (3 -> . . .) for ($i = 0; $i < strlen($row); $i++) { if (is_numeric($row[$i])) { $line .= str_repeat(" .", intval($row[$i])); } else { $line .= " " . $row[$i]; } } $visual .= sprintf("%2d |%s |\n", $index, $line); } $visual .= " -------------------\n"; $visual .= "Ghi chú: Chữ Hoa (R,N,C...) = Đỏ (Dưới); Chữ thường (r,n,c...) = Đen (Trên).\n"; return $visual; } /** * Hàm gọi API đệ quy với cơ chế Fallback (Index-based) giống GroqService. * @param array $messages Lịch sử chat gửi lên AI. * @param int $modelIndex Chỉ mục của model trong mảng availableModels đang thử. * @param bool $jsonMode Có bắt buộc trả về JSON không. * @param float $temperature Độ sáng tạo. * @return string|null Nội dung trả lời hoặc null nếu thất bại hoàn toàn. */ protected function callWithFallback(array $messages, int $modelIndex = 0, bool $jsonMode = false, float $temperature = 1) { // 1. Kiểm tra điều kiện dừng: Nếu index vượt quá số lượng model khả dụng if (!isset($this->availableModels[$modelIndex])) { Log::error("GroqChessService: All models failed. Last attempted index: " . ($modelIndex - 1)); return null; } // 2. Lấy model hiện tại theo index $currentModel = $this->availableModels[$modelIndex]; try { // Cấu hình payload gửi đi $payload = [ 'model' => $currentModel, 'messages' => $messages, 'temperature' => $temperature, 'max_tokens' => 4096, ]; if ($jsonMode) { $payload['response_format'] = ['type' => 'json_object']; } // Thực hiện gọi HTTP Request $response = Http::withToken($this->apiKey) ->timeout(30) ->post($this->baseUrl, $payload); // 3. Xử lý kết quả // TRƯỜNG HỢP: THÀNH CÔNG (200 OK) if ($response->successful()) { return $response->json()['choices'][0]['message']['content']; } // TRƯỜNG HỢP: LỖI CÓ THỂ THỬ LẠI (429, 5xx) if (in_array($response->status(), [429, 500, 502, 503, 504])) { // [NEW] Xử lý Retry-After Header if ($response->status() === 429) { $retryAfter = $response->header('Retry-After'); if ($retryAfter) { $seconds = (int) $retryAfter; // Nếu thời gian chờ hợp lý (ví dụ < 10s), ta sleep để tôn trọng API // Nếu quá lâu, vẫn sleep để tránh spam rồi chuyển model khác Log::warning("GroqChessService: Rate limit hit (429). Sleeping for {$seconds}s based on Retry-After header."); sleep($seconds); } } Log::warning("GroqChessService Model [$currentModel] failed with status {$response->status()}. Switching to next model..."); // ĐỆ QUY: Thử model tiếp theo (Index + 1) return $this->callWithFallback($messages, $modelIndex + 1, $jsonMode, $temperature); } // TRƯỜNG HỢP: LỖI KHÔNG THỂ THỬ LẠI (400, 401...) Log::error("GroqChessService API Permanent Error [$currentModel]: " . $response->body()); return null; } catch (\Exception $e) { // Lỗi kết nối hoặc timeout -> Thử model tiếp theo Log::warning("GroqChessService Exception [$currentModel]: " . $e->getMessage()); // ĐỆ QUY: Thử model tiếp theo (Index + 1) return $this->callWithFallback($messages, $modelIndex + 1, $jsonMode, $temperature); } } /** * Phân tích ván cờ với sự hỗ trợ từ Engine * @param string $fen Mã FEN của thế cờ * @param string|null $engineBestMove Nước đi tốt nhất từ Pikafish (nếu có) */ public function analyzeGame($fen, $engineBestMove = null) { $visualBoard = $this->fenToVisualBoard($fen); $engineContext = ""; if ($engineBestMove) { $engineContext = "\n### THAM KHẢO TỪ ENGINE (PIKAFISH)\n- Engine đã tính toán và đề xuất nước đi tối ưu (UCI Code): **$engineBestMove**.\n- Nhiệm vụ của bạn: Hãy ưu tiên phân tích nước đi này, chuyển đổi nó sang ký hiệu Tiếng Việt chuẩn (Tấn/Thoái/Bình) và giải thích tại sao nó hay. Dùng ký hiệu chuẩn đã nêu ở phần Common Rules."; } // [FIX] Đưa FEN vào System Prompt để AI nhận diện đây là bối cảnh bắt buộc // Kết hợp Common Rules với nhiệm vụ cụ thể của hàm Analyze $systemPrompt = $this->commonRules . <<commonRules . << 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userPrompt], ]; // Gọi hàm đệ quy với danh sách model đầy đủ return $this->callWithFallback($messages, 0, true, 1); } /** * Chat với AI Coach dựa trên ngữ cảnh FEN * @param string $fen Mã FEN hiện tại * @param string $userQuestion Câu hỏi của người dùng */ public function chatWithCoach($fen, $userQuestion, $engineBestMove = null) { $visualBoard = $this->fenToVisualBoard($fen); $engineContext = ""; if ($engineBestMove) { $engineContext = "\n### DỮ LIỆU TỪ ENGINE (PIKAFISH)\n- Engine đã tính toán chính xác nước đi tốt nhất là: **$engineBestMove** (UCI Code).\n- YÊU CẦU BẮT BUỘC: Bạn phải khuyên người chơi đi nước này. Hãy dịch nó sang ký hiệu Tiếng Việt (Tấn/Thoái/Bình) và giải thích ngắn gọn tại sao nó hay. Dùng ký hiệu chuẩn đã nêu ở phần Common Rules."; } // [FIX] Cập nhật System Prompt chứa FEN để đảm bảo ngữ cảnh đúng // Kết hợp Common Rules với ngữ cảnh chat cụ thể $systemPrompt = $this->commonRules . <<commonRules . << 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userPrompt], ]; // Gọi hàm đệ quy $result = $this->callWithFallback($messages, 0, false, 1); return $result ?? "Xin lỗi, hiện tại tất cả các AI đều đang bận (Lỗi kết nối)."; } }