Files
sam-kd/chatbot/api.php

181 lines
6.6 KiB
PHP
Raw Permalink Normal View History

<?php
require_once($_SERVER['DOCUMENT_ROOT'] . "/session.php");
header('Content-Type: application/json; charset=utf-8');
// POST 요청만 처리
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['error' => 'Invalid request method']);
exit;
}
// JSON 입력 받기
$input = json_decode(file_get_contents('php://input'), true);
$userMessage = $input['message'] ?? '';
$history = $input['history'] ?? [];
if (empty($userMessage)) {
echo json_encode(['error' => 'Empty message']);
exit;
}
// API 키 설정
$notionApiKeyPath = $_SERVER['DOCUMENT_ROOT'] . "/apikey/notion.txt";
$googleApiKeyPath = $_SERVER['DOCUMENT_ROOT'] . "/apikey/google_vertex_api.txt";
if (!file_exists($notionApiKeyPath) || !file_exists($googleApiKeyPath)) {
echo json_encode(['reply' => "시스템 설정 오류: API 키 파일을 찾을 수 없습니다."]);
exit;
}
$notionApiKey = trim(file_get_contents($notionApiKeyPath));
$googleApiKey = trim(file_get_contents($googleApiKeyPath));
require_once 'notion_client.php';
try {
// 0. 검색어 정제 및 확장 (Query Refinement)
// 항상 Gemini를 이용해 오타 수정 및 Notion 검색에 적합한 키워드로 변환합니다.
$refineSystemInstruction = "You are a detailed search query generator for a Notion database.
Analyze the [Current Question] and [Conversation History] (if any).
1. Correct any typos (e.g., '프론트엔디' -> '프론트엔드').
2. Identify the core topic or entities.
3. IGNORE standard greetings (e.g., 'Hello', '운영자 문서 탐색').
4. Convert the intent into a precise search query likely to match Notion page titles or content.
RETURN ONLY THE SEARCH QUERY STRING. Do not explain.";
$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";
}
}
$refineData = [
'contents' => [
['parts' => [['text' => "Conversation History:\n$historyText\n\nCurrent Question: $userMessage"]]]
],
'systemInstruction' => [
'parts' => [['text' => $refineSystemInstruction]]
]
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=" . $googleApiKey);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($refineData));
$refineResponse = curl_exec($ch);
curl_close($ch);
$refineData = json_decode($refineResponse, true);
$refinedQuery = $userMessage; // 기본값
if (isset($refineData['candidates'][0]['content']['parts'][0]['text'])) {
$refinedQuery = trim($refineData['candidates'][0]['content']['parts'][0]['text']);
}
// 1. Notion 검색 및 컨텍스트 확보 (정제된 쿼리 사용)
$notion = new NotionClient($notionApiKey);
$searchResults = $notion->search($refinedQuery);
$context = "";
if ($searchResults && isset($searchResults['results'])) {
foreach ($searchResults['results'] as $page) {
$title = "제목 없음";
if (isset($page['properties']['Name']['title'][0]['plain_text'])) {
$title = $page['properties']['Name']['title'][0]['plain_text'];
} elseif (isset($page['properties']['title']['title'][0]['plain_text'])) {
$title = $page['properties']['title']['title'][0]['plain_text'];
}
$pageContent = $notion->getPageContent($page['id']);
$url = $page['url'] ?? '';
$context .= "문서 제목: [{$title}]\nURL: {$url}\n내용:\n{$pageContent}\n---\n";
}
}
if (empty($context)) {
$context = "관련된 내부 문서를 찾을 수 없습니다.";
}
// 2. Gemini API 호출
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=" . $googleApiKey;
$headers = [
'Content-Type: application/json'
];
$systemInstruction = "You are a helpful, friendly, and professional customer support agent for 'codebridge-x.com'. Your tone should be polite and efficient, similar to Korean customer service standards. Use the provided [Context] to answer the user's question. If the context doesn't contain the answer, say you don't have that information in the internal documents. Reply in Korean.\n";
// 이력 포함
if (!empty($historyText)) {
$systemInstruction .= "\n[Conversation History]\n" . $historyText;
}
$systemInstruction .= "\nIMPORTANT: Even if you cannot find the direct answer in the Context, you MUST list the documents provided in the Context as '관련 문서' at the bottom.
If the document content is long or partial (ends with '...'), summarize the key points available and encourage the user to click the link for full details.
Format:
[Answer / Summary]
관련 문서:
- [문서 제목](URL)
[Context]\n" . $context;
$data = [
'contents' => [
[
'parts' => [
['text' => $userMessage]
]
]
],
'systemInstruction' => [
'parts' => [
['text' => $systemInstruction]
]
]
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
throw new Exception(curl_error($ch));
}
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception("API Error: " . $response);
}
$responseData = json_decode($response, true);
$reply = $responseData['candidates'][0]['content']['parts'][0]['text'] ?? "죄송합니다. 답변을 생성하지 못했습니다.";
echo json_encode([
'reply' => $reply,
'debug' => [
'refinedQuery' => $refinedQuery,
'context' => $context,
'systemInstruction' => $systemInstruction,
'rawResponse' => $responseData
]
]);
} catch (Exception $e) {
echo json_encode(['reply' => "오류가 발생했습니다: " . $e->getMessage()]);
}
?>