'Invalid request method']); exit; } $input = json_decode(file_get_contents('php://input'), true); $userMessage = $input['message'] ?? ''; $history = $input['history'] ?? []; if (empty($userMessage)) { ob_clean(); echo json_encode(['error' => 'Empty message']); exit; } $googleApiKeyPath = $_SERVER['DOCUMENT_ROOT'] . "/apikey/google_vertex_api.txt"; if (!file_exists($googleApiKeyPath)) { ob_clean(); echo json_encode(['reply' => "API Key not found."]); exit; } $googleApiKey = trim(file_get_contents($googleApiKeyPath)); require_once __DIR__ . '/rag/search.php'; try { // 1. Vector Search (Semantic Search) // 기존 NotionClient->search() 대신 VectorSearch 사용 $vectorSearch = new VectorSearch(); $results = $vectorSearch->search($userMessage, $googleApiKey, 5); // 상위 5개 $context = ""; if (empty($results)) { $context = "관련된 내부 문서를 찾을 수 없습니다. (벡터 데이터가 없거나 매칭 실패)"; } else { // Deduplicate documents (Group text by URL) $processedDocs = []; foreach ($results as $doc) { $url = $doc['url']; if (!isset($processedDocs[$url])) { $processedDocs[$url] = [ 'title' => $doc['title'], 'score' => $doc['score'], 'text' => $doc['text'] ]; } else { // Determine if this text chunk is already in the compiled text to avoid exact duplication // (Simple check, can be improved) if (strpos($processedDocs[$url]['text'], $doc['text']) === false) { $processedDocs[$url]['text'] .= "\n[...추가 내용...]\n" . $doc['text']; } } } foreach ($processedDocs as $url => $doc) { $score = round($doc['score'] * 100, 1); $context .= "문서 제목: [{$doc['title']}] (유사도: {$score}%)\nURL: {$url}\n내용:\n{$doc['text']}\n---\n"; } } // 2. Gemini Answer Generation $url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=" . $googleApiKey; $headers = ['Content-Type: application/json']; $historyText = ""; if (!empty($history)) { foreach ($history as $msg) { $role = $msg['role'] === 'user' ? "User" : "Assistant"; $text = is_array($msg['parts']) ? $msg['parts'][0]['text'] : $msg['parts']; $historyText .= "$role: $text\n"; } } $systemInstruction = "You are a helpful customer support agent for 'codebridge-x.com'. Use the provided [Context] (retrieved via Semantic Vector Search) to answer the user's question. If you cannot find the answer in the context, say so. [Conversation History] $historyText IMPORTANT: List the used documents at the bottom. Format: [Answer] 관련 문서 (Vector Search Result): - [Title](URL) [Context] $context"; $data = [ 'contents' => [['parts' => [['text' => $userMessage]]]], 'systemInstruction' => ['parts' => [['text' => $systemInstruction]]] ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); if (curl_errno($ch)) throw new Exception(curl_error($ch)); curl_close($ch); $responseData = json_decode($response, true); // Check for standard text response $reply = $responseData['candidates'][0]['content']['parts'][0]['text'] ?? null; // If empty, check for finishReason if (!$reply) { $finishReason = $responseData['candidates'][0]['finishReason'] ?? 'UNKNOWN'; if ($finishReason === 'SAFETY') { $reply = "죄송합니다. 해당 질문에 대한 답변은 안전 정책상 제공해드릴 수 없습니다."; } else { $reply = "답변 생성 실패 (Finish Reason: $finishReason). Raw Response를 확인해주세요."; } } // Debug File Status $vectorFile = __DIR__ . '/rag/data/vectors.json'; $fileStatus = [ 'path' => $vectorFile, 'exists' => file_exists($vectorFile), 'size' => file_exists($vectorFile) ? filesize($vectorFile) : -1, 'memory_limit' => ini_get('memory_limit') ]; ob_clean(); echo json_encode([ 'reply' => $reply, 'debug' => [ 'refinedQuery' => $userMessage, 'vectorCount' => $vectorSearch->getVectorCount(), 'loadError' => $vectorSearch->getLastError(), 'fileStatus' => $fileStatus, 'context' => $context, 'systemInstruction' => $systemInstruction, 'rawResponse' => $responseData ] ]); } catch (Exception $e) { ob_clean(); echo json_encode(['reply' => "Error: " . $e->getMessage()]); } ?>