-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaiapi.php
More file actions
258 lines (226 loc) · 12.1 KB
/
aiapi.php
File metadata and controls
258 lines (226 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
<?php
// aiapi.php - Dedicated backend proxy for Gemini API calls (v3 - Robust Output)
// Uses output buffering to prevent stray output from corrupting JSON response.
// --- Start Output Buffering ---
// Capture any potential premature output (warnings, notices, whitespace)
ob_start();
// --- Constants ---
// Define paths relative to this script's directory for robustness
define('SCRIPT_DIR', __DIR__);
define('PROMPT_FILE_PATH', SCRIPT_DIR . '/prompt.php');
// For external config file method (adjust path as needed)
// define('CONFIG_FILE_PATH', SCRIPT_DIR . '/../pythai_secure_config/gemini_config.php');
// --- Method Check ---
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
ob_clean(); // Clean buffer before error output
header('Content-Type: application/json');
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method Not Allowed. Only POST requests are accepted.']);
exit;
}
// --- Input Validation ---
$user_prompt = filter_input(INPUT_POST, 'prompt', FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
if (empty(trim((string)$user_prompt))) { // Ensure it's not just whitespace
ob_clean(); // Clean buffer before error output
header('Content-Type: application/json');
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Bad Request: Missing or empty prompt data.']);
exit;
}
// --- Include PYTHAI Personality Prompt ---
if (is_readable(PROMPT_FILE_PATH)) {
require_once PROMPT_FILE_PATH; // Should define $systemPrompt
} else {
error_log("FATAL: Prompt file not found or not readable at: " . PROMPT_FILE_PATH);
ob_clean(); // Clean buffer before error output
header('Content-Type: application/json');
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Internal Server Error: Personality definition file is missing.']);
exit;
}
if (!isset($systemPrompt) || !is_string($systemPrompt) || empty(trim($systemPrompt))) {
error_log("FATAL: \$systemPrompt variable was not defined correctly or is empty after including prompt.php.");
ob_clean(); // Clean buffer before error output
header('Content-Type: application/json');
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Internal Server Error: Personality definition is invalid or empty.']);
exit;
}
// --- Model Selection Logic ---
// Use full model names including 'models/' prefix as required by the API endpoint structure
$allowedTextModels = [
'models/gemini-2.5-pro-preview-03-25', // The one Google recommends for higher quotas
'models/gemini-1.5-pro-latest',
'models/gemini-1.5-flash-latest',
'models/gemini-pro' // This is equivalent to 'models/gemini-1.0-pro'
// Add other specific, fully-qualified model names from Google's documentation if needed
// e.g., 'models/gemini-1.0-pro-vision-latest' (if using vision capabilities)
// Check https://ai.google.dev/models/gemini for the latest model names
];
// Set default to the recommended model for higher quotas
$defaultModel = 'models/gemini-2.5-pro-preview-03-25';
$requestedModelInput = filter_input(INPUT_POST, 'model', FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
$selectedModel = $defaultModel;
if (!empty($requestedModelInput)) {
// For robustness, ensure the client sends the full model name (e.g., 'models/gemini-1.5-pro-latest')
// If clients might send short names (e.g. 'gemini-1.5-pro-latest'), you'd need to prefix 'models/'
// before checking against $allowedTextModels, or adjust $allowedTextModels.
// Current assumption: client sends the full 'models/...' name if overriding default.
if (in_array($requestedModelInput, $allowedTextModels)) {
$selectedModel = $requestedModelInput;
} else {
error_log("Warning: Invalid or unsupported model requested ('{$requestedModelInput}'), using default ('{$defaultModel}').");
// Fallback to default is already handled by $selectedModel = $defaultModel initialization
}
}
// --- API Key Security ---
$apiKey = getenv('GEMINI_API_KEY');
/* // Alt: require CONFIG_FILE_PATH; // if ($apiKey = $config['GEMINI_API_KEY'] ?? null) */
if (empty($apiKey)) {
error_log("FATAL: GEMINI_API_KEY is not configured on the server (checked environment variable).");
ob_clean(); // Clean buffer before error output
header('Content-Type: application/json');
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Internal Server Error: AI service API Key not configured.']);
exit;
}
// --- Prepare Gemini API Call ---
// CORRECTED: $selectedModel already contains 'models/model-name'
$geminiApiUrl = 'https://generativelanguage.googleapis.com/v1beta/' . $selectedModel . ':generateContent?key=' . $apiKey;
$finalPrompt = $systemPrompt . "\n\n---\n\nUSER QUERY:\n" . $user_prompt;
$data = [
'contents' => [['role' => 'user', 'parts' => [['text' => $finalPrompt]]]],
// Optional: Add 'generationConfig' or 'safetySettings' here if needed
// 'generationConfig' => [
// 'temperature' => 0.7,
// 'maxOutputTokens' => 1024,
// ],
// 'safetySettings' => [
// [ 'category' => 'HARM_CATEGORY_HARASSMENT', 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE' ],
// // ... other categories
// ]
];
$jsonData = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($jsonData === false) {
error_log('JSON Encode Error for request payload: ' . json_last_error_msg());
ob_clean(); // Clean buffer before error output
header('Content-Type: application/json');
http_response_code(500);
echo json_encode(['success'=>false,'message'=>'Internal Server Error: Failed encoding request data.']);
exit;
}
// --- Execute cURL Request ---
$ch = curl_init($geminiApiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Accept: application/json',
'Content-Length: ' . strlen($jsonData)
]);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20); // Increased connect timeout
curl_setopt($ch, CURLOPT_TIMEOUT, 120); // Increased total timeout for potentially long AI responses
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
// For debugging cURL headers if needed:
// curl_setopt($ch, CURLOPT_VERBOSE, true);
// $verbose = fopen('php://temp', 'w+');
// curl_setopt($ch, CURLOPT_STDERR, $verbose);
$apiResponse = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErrorNumber = curl_errno($ch);
$curlErrorMessage = $curlErrorNumber ? curl_error($ch) : null;
// if (isset($verbose)) {
// rewind($verbose);
// $verboseLog = stream_get_contents($verbose);
// error_log("cURL Verbose Information:\n" . $verboseLog);
// fclose($verbose);
// }
curl_close($ch);
// --- Handle cURL/Network Error ---
if ($curlErrorNumber) {
error_log('cURL Error calling Gemini (Code: ' . $curlErrorNumber . '): ' . $curlErrorMessage . ' | URL: ' . $geminiApiUrl);
ob_clean(); // Clean buffer before error output
header('Content-Type: application/json');
http_response_code(504); // Gateway Timeout or other connection issue
echo json_encode(['success'=>false,'message'=>'Gateway Error: Cannot connect to AI service. Please try again later.', 'details' => 'cURL error: ' . $curlErrorMessage]);
exit;
}
// --- Process Gemini API Response ---
$responseData = json_decode($apiResponse, true);
$jsonDecodeError = json_last_error();
// Check JSON decode error (meaning API returned non-JSON or malformed JSON)
if ($jsonDecodeError !== JSON_ERROR_NONE) {
error_log('JSON Decode Error for API response: ' . json_last_error_msg() . ' | HTTP Code: ' . $httpCode . ' | Model: ' . $selectedModel . ' | Raw Response: '.$apiResponse);
ob_clean(); // Clean buffer before error output
header('Content-Type: application/json');
http_response_code(502); // Bad Gateway (invalid response from upstream server)
echo json_encode(['success'=>false,'message'=>'Bad Gateway: Invalid AI response format received.', 'details' => 'Could not parse JSON from AI service.']);
exit;
}
// Check API error reported in JSON / or HTTP error codes
if ($httpCode >= 400 || isset($responseData['error'])) {
$errorMessage = $responseData['error']['message'] ?? 'AI service returned an unspecified error.';
$errorDetails = $responseData['error'] ?? ['code' => $httpCode, 'message' => 'HTTP error code indicated failure.'];
if ($httpCode === 429) { // Specifically for rate limiting / quota
$errorMessage = 'AI service quota exceeded or rate limit hit. Please try again later.';
if (isset($responseData['error']['details'])) {
// Google sometimes provides more info here, e.g., which quota was hit.
$errorMessage .= " (Details: " . json_encode($responseData['error']['details']) . ")";
}
}
error_log("Gemini API Error (Model: {$selectedModel}, HTTP: {$httpCode}): " . json_encode($responseData)); // Log the full JSON error
ob_clean(); // Clean buffer before error output
header('Content-Type: application/json');
// Use the API's error code if available and valid, otherwise the HTTP code, or default to 502
$responseHttpCode = $responseData['error']['code'] ?? $httpCode;
if ($responseHttpCode < 400) $responseHttpCode = 502; // Ensure it's an error code
http_response_code($responseHttpCode);
echo json_encode(['success' => false, 'message' => $errorMessage, 'details' => $errorDetails, 'modelUsed' => $selectedModel]);
exit;
}
// --- Extract Text Safely ---
$aiText = null;
if (isset($responseData['candidates'][0]['content']['parts'][0]['text'])) {
$aiText = $responseData['candidates'][0]['content']['parts'][0]['text'];
} else {
$finishReason = $responseData['candidates'][0]['finishReason'] ?? 'UNKNOWN';
$isBlocked = in_array($finishReason, ['SAFETY', 'RECITATION']);
$blockMessage = '';
if ($finishReason === 'SAFETY') { $blockMessage = 'AI response blocked by safety filters.'; }
elseif ($finishReason === 'RECITATION') { $blockMessage = 'AI response blocked due to potential recitation issues.'; }
error_log("Could not extract text from AI response (Model: {$selectedModel}). FinishReason: {$finishReason} | Full Response: " . json_encode($responseData));
$messageToUser = 'Internal Server Error: Could not extract valid text from AI response.';
if ($isBlocked) {
$messageToUser = $blockMessage;
} elseif (!in_array($finishReason, ['STOP', 'MAX_TOKENS', 'OTHER'])) { // 'OTHER' can be a valid non-error finish
$messageToUser = 'AI response generation finished unexpectedly. Reason: ' . $finishReason;
}
ob_clean(); // Clean buffer before error output
header('Content-Type: application/json');
// For blocked content or other non-STOP finish reasons, it's often best to return HTTP 200
// with success:false, as the API call itself didn't fail at a transport/auth level.
// A 500 might imply your server failed, not that the AI content was problematic.
http_response_code(200);
echo json_encode(['success' => false, 'message' => $messageToUser, 'finishReason' => $finishReason, 'modelUsed' => $selectedModel]);
exit;
}
// --- Send SUCCESS Response ---
ob_clean(); // **Discard any previous buffered output**
header('Content-Type: application/json'); // Set header *just before* final output
$successPayload = ['success' => true, 'response' => $aiText, 'modelUsed' => $selectedModel];
$jsonSuccess = json_encode($successPayload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
// Final check if encoding the success payload somehow failed (very unlikely here)
if ($jsonSuccess === false) {
error_log('Failed to encode SUCCESS JSON payload: ' . json_last_error_msg());
// At this point, headers might have already been sent partially if ob_clean failed.
// Best effort to send a plain text error.
http_response_code(500);
header('Content-Type: text/plain'); // Attempt to override if possible
echo 'Internal Server Error: Failed finalizing successful response.';
} else {
echo $jsonSuccess;
}
exit; // Final exit
?>