| 1 |
<?php |
| 2 |
if (!defined('ABSPATH')) exit; // Exit if accessed directly |
| 3 |
if ( ! class_exists( 'Qcld_Bot_Rag' ) ) { |
| 4 |
class Qcld_Bot_Rag{ |
| 5 |
private $api_key; |
| 6 |
private $baseUrl; |
| 7 |
|
| 8 |
/** |
| 9 |
* @var self |
| 10 |
*/ |
| 11 |
private static $_instance = null; |
| 12 |
|
| 13 |
/** |
| 14 |
* @return self |
| 15 |
*/ |
| 16 |
public static function instance() { |
| 17 |
if ( is_null( self::$_instance ) ) { |
| 18 |
self::$_instance = new self(); |
| 19 |
} |
| 20 |
return self::$_instance; |
| 21 |
} |
| 22 |
public function __construct() { |
| 23 |
$this->check_api_endpoint(); |
| 24 |
add_action('wp_ajax_rag_upload_pdf', array($this, 'ajax_rag_upload_pdf')); |
| 25 |
add_action('wp_ajax_rag_upload_csv', array($this, 'ajax_rag_upload_csv')); |
| 26 |
add_action('wp_ajax_rag_upload_xaml', array($this, 'ajax_rag_upload_xaml')); |
| 27 |
add_action('wp_ajax_qcld_rag_manual_sync', array($this, 'ajax_rag_manual_sync')); |
| 28 |
add_action('wp_ajax_qcld_rag_delete_document', array($this, 'qcld_rag_delete_document_callback')); |
| 29 |
add_action('wp_ajax_qcld_rag_get_document', array($this, 'qcld_rag_get_document_callback')); |
| 30 |
add_action('wp_ajax_qcld_rag_update_document', array($this, 'qcld_rag_update_document_callback')); |
| 31 |
add_action('wp_ajax_qcld_rag_bulk_delete_documents', array($this, 'qcld_rag_bulk_delete_documents_callback')); |
| 32 |
add_action('wp_ajax_qcld_rag_delete_all_documents', array($this, 'qcld_rag_delete_all_documents_callback')); |
| 33 |
add_action('wp_ajax_qcld_rag_get_embed_queue', array($this, 'ajax_qcld_rag_get_embed_queue')); |
| 34 |
add_action('wp_ajax_qcld_rag_process_item', array($this, 'ajax_qcld_rag_process_item')); |
| 35 |
add_action('save_post', array($this, 'wp_rag_handle_auto_sync_hook'), 10, 3); |
| 36 |
} |
| 37 |
|
| 38 |
public function check_api_endpoint() { |
| 39 |
if (get_option('qcld_gemini_enabled') == '1') { |
| 40 |
$this->api_key = get_option('qcld_gemini_api_key'); |
| 41 |
$this->baseUrl = 'https://generativelanguage.googleapis.com/v1beta'; |
| 42 |
} else if (get_option('ai_enabled') == '1') { |
| 43 |
$this->api_key = get_option('open_ai_api_key'); |
| 44 |
$this->baseUrl = 'https://api.openai.com/v1/'; |
| 45 |
} else if (get_option('qcld_openrouter_enabled') == '1') { |
| 46 |
$this->api_key = get_option('qcld_openrouter_api_key'); |
| 47 |
$this->baseUrl = 'https://openrouter.ai/api/v1/'; |
| 48 |
} else if (get_option('qcld_grok_enabled') == '1') { |
| 49 |
$this->api_key = get_option('qcld_grok_api_key'); |
| 50 |
$this->baseUrl = 'https://api.x.ai/v1/'; |
| 51 |
} else { |
| 52 |
$this->api_key = ''; |
| 53 |
$this->baseUrl = ''; |
| 54 |
} |
| 55 |
|
| 56 |
if (empty($this->api_key)) { |
| 57 |
return new WP_Error('no_api_key', 'No API key is set.'); |
| 58 |
} |
| 59 |
return true; |
| 60 |
} |
| 61 |
public function generate_embedding($text) { |
| 62 |
if (!empty($this->api_key) && get_option('ai_enabled') == '1') { |
| 63 |
return $this->generate_openai_embedding($text); |
| 64 |
} else if (!empty($this->api_key) && get_option('qcld_gemini_enabled') == '1') { |
| 65 |
return $this->generate_gemini_embedding($text); |
| 66 |
} else if (!empty($this->api_key) && get_option('qcld_openrouter_enabled') == '1' || ($this->baseUrl == 'https://openrouter.ai/api/v1/')) { |
| 67 |
return $this->generate_openrouter_embedding($text); |
| 68 |
} else if (!empty($this->api_key) && get_option('qcld_grok_enabled') == '1' || ($this->baseUrl == 'https://api.x.ai/v1/')) { |
| 69 |
return $this->generate_xai_embedding($text); |
| 70 |
} |
| 71 |
} |
| 72 |
|
| 73 |
public function generate_xai_embedding( $text, $model = 'text-embedding-3-small' ) { |
| 74 |
|
| 75 |
if (empty($this->api_key)) { |
| 76 |
$this->api_key = get_option('qcld_grok_api_key'); |
| 77 |
} |
| 78 |
|
| 79 |
if (empty($this->api_key)) { |
| 80 |
return []; |
| 81 |
} |
| 82 |
|
| 83 |
$response = wp_remote_post( |
| 84 |
"https://api.x.ai/v1/embeddings", |
| 85 |
[ |
| 86 |
'headers' => [ |
| 87 |
'Authorization' => "Bearer {$this->api_key}", |
| 88 |
'Content-Type' => 'application/json' |
| 89 |
], |
| 90 |
'body' => wp_json_encode([ |
| 91 |
'model' => $model, |
| 92 |
'input' => mb_substr($text, 0, 15000) // keep within token limits |
| 93 |
]) |
| 94 |
] |
| 95 |
); |
| 96 |
|
| 97 |
if (is_wp_error($response)) return $response; |
| 98 |
|
| 99 |
$body = json_decode(wp_remote_retrieve_body($response), true); |
| 100 |
|
| 101 |
if (isset($body['data'][0]['embedding'])) { |
| 102 |
return $body['data'][0]['embedding']; |
| 103 |
} |
| 104 |
|
| 105 |
if (isset($body['error']['message'])) { |
| 106 |
return new WP_Error('xai_error', 'xAI Error: ' . $body['error']['message']); |
| 107 |
} |
| 108 |
|
| 109 |
return new WP_Error('invalid_response', 'Invalid response format from xAI API'); |
| 110 |
} |
| 111 |
|
| 112 |
public function generate_openrouter_embedding( $text, $model = 'openai/text-embedding-3-small' ) { |
| 113 |
|
| 114 |
if (empty($this->api_key)) { |
| 115 |
$this->api_key = get_option('qcld_openrouter_api_key'); |
| 116 |
} |
| 117 |
|
| 118 |
if (empty($this->api_key)) { |
| 119 |
return []; |
| 120 |
} |
| 121 |
|
| 122 |
$response = wp_remote_post( |
| 123 |
"https://openrouter.ai/api/v1/embeddings", |
| 124 |
[ |
| 125 |
'headers' => [ |
| 126 |
'Authorization' => "Bearer {$this->api_key}", |
| 127 |
'Content-Type' => 'application/json' |
| 128 |
], |
| 129 |
'body' => wp_json_encode([ |
| 130 |
'model' => $model, |
| 131 |
'input' => mb_substr($text, 0, 15000) // keep within token limits |
| 132 |
]) |
| 133 |
] |
| 134 |
); |
| 135 |
|
| 136 |
if (is_wp_error($response)) return $response; |
| 137 |
|
| 138 |
$body = json_decode(wp_remote_retrieve_body($response), true); |
| 139 |
|
| 140 |
if (isset($body['data'][0]['embedding'])) { |
| 141 |
return $body['data'][0]['embedding']; |
| 142 |
} |
| 143 |
|
| 144 |
if (isset($body['error']['message'])) { |
| 145 |
return new WP_Error('openrouter_error', 'OpenRouter Error: ' . $body['error']['message']); |
| 146 |
} |
| 147 |
|
| 148 |
if (isset($body['error']) && is_string($body['error'])) { |
| 149 |
return new WP_Error('openrouter_error', 'OpenRouter Error: ' . $body['error']); |
| 150 |
} |
| 151 |
|
| 152 |
return new WP_Error('invalid_response', 'Invalid response format from OpenRouter API'); |
| 153 |
} |
| 154 |
public function generate_gemini_embedding($text, $model = 'text-embedding-004') { |
| 155 |
$url = "{$this->baseUrl}/models/{$model}:embedContent?key={$this->api_key}"; |
| 156 |
|
| 157 |
$data = [ |
| 158 |
'model' => "models/{$model}", |
| 159 |
'content' => [ |
| 160 |
'parts' => [ |
| 161 |
['text' => $text] |
| 162 |
] |
| 163 |
] |
| 164 |
]; |
| 165 |
|
| 166 |
$response_raw = wp_remote_post( |
| 167 |
$url, |
| 168 |
array( |
| 169 |
'headers' => array( 'Content-Type' => 'application/json' ), |
| 170 |
'body' => wp_json_encode( $data ), |
| 171 |
'timeout' => 30, |
| 172 |
) |
| 173 |
); |
| 174 |
|
| 175 |
if ( is_wp_error( $response_raw ) ) { |
| 176 |
return []; |
| 177 |
} |
| 178 |
|
| 179 |
$httpCode = wp_remote_retrieve_response_code( $response_raw ); |
| 180 |
if ( $httpCode !== 200 ) { |
| 181 |
return []; |
| 182 |
} |
| 183 |
|
| 184 |
$result = json_decode( wp_remote_retrieve_body( $response_raw ), true ); |
| 185 |
if (isset($result['embedding']['values'])) { |
| 186 |
return $result['embedding']['values']; |
| 187 |
} |
| 188 |
|
| 189 |
// throw new Exception("No embedding in response: $response"); |
| 190 |
return []; |
| 191 |
} |
| 192 |
// Generate OpenAI embedding for given text |
| 193 |
public function generate_openai_embedding($text) { |
| 194 |
|
| 195 |
|
| 196 |
$response = wp_remote_post( |
| 197 |
"https://api.openai.com/v1/embeddings", |
| 198 |
[ |
| 199 |
'headers' => [ |
| 200 |
'Authorization' => "Bearer {$this->api_key}", |
| 201 |
'Content-Type' => 'application/json' |
| 202 |
], |
| 203 |
'body' => wp_json_encode([ |
| 204 |
'model' => 'text-embedding-3-large', |
| 205 |
'input' => mb_substr($text, 0, 15000) // keep within token limits |
| 206 |
]) |
| 207 |
] |
| 208 |
); |
| 209 |
|
| 210 |
if (is_wp_error($response)) return []; |
| 211 |
|
| 212 |
$body = json_decode(wp_remote_retrieve_body($response), true); |
| 213 |
|
| 214 |
return $body['data'][0]['embedding'] ?? []; |
| 215 |
} |
| 216 |
// AJAX handler for PDF upload |
| 217 |
public function ajax_rag_upload_pdf() { |
| 218 |
check_ajax_referer('rag_upload_nonce', 'nonce'); |
| 219 |
|
| 220 |
if (empty($_FILES['rag_pdf'])) { |
| 221 |
wp_send_json_error(['message' => 'No PDF files uploaded']); |
| 222 |
} |
| 223 |
|
| 224 |
ob_start(); |
| 225 |
$this->wp_rag_process_pdf_upload(); |
| 226 |
$output = ob_get_clean(); |
| 227 |
|
| 228 |
wp_send_json_success(['message' => 'PDF processing complete', 'output' => $output]); |
| 229 |
} |
| 230 |
|
| 231 |
// AJAX handler for XAML upload |
| 232 |
public function ajax_rag_upload_xaml() { |
| 233 |
check_ajax_referer('rag_upload_nonce', 'nonce'); |
| 234 |
|
| 235 |
if (empty($_FILES['rag_xaml'])) { |
| 236 |
wp_send_json_error(['message' => 'No XAML files uploaded']); |
| 237 |
} |
| 238 |
|
| 239 |
ob_start(); |
| 240 |
$this->wp_rag_process_xaml_upload(); |
| 241 |
$output = ob_get_clean(); |
| 242 |
|
| 243 |
wp_send_json_success(['message' => 'XAML processing complete', 'output' => $output]); |
| 244 |
} |
| 245 |
|
| 246 |
// AJAX handler for CSV upload |
| 247 |
public function ajax_rag_upload_csv() { |
| 248 |
check_ajax_referer('rag_upload_nonce', 'nonce'); |
| 249 |
|
| 250 |
if (empty($_FILES['rag_csv'])) { |
| 251 |
wp_send_json_error(['message' => 'No CSV files uploaded']); |
| 252 |
} |
| 253 |
|
| 254 |
// ob_start(); |
| 255 |
$this->wp_rag_process_csv_upload(); |
| 256 |
// $output = ob_get_clean(); |
| 257 |
|
| 258 |
// wp_send_json_success(['message' => 'CSV processing complete', 'output' => $output]); |
| 259 |
} |
| 260 |
|
| 261 |
// CSV processing method |
| 262 |
public function wp_rag_process_csv_upload() { |
| 263 |
if (empty($_FILES['rag_csv']['name'][0])) { |
| 264 |
echo "<p>No CSV selected.</p>"; |
| 265 |
return; |
| 266 |
} |
| 267 |
|
| 268 |
require_once(ABSPATH . 'wp-admin/includes/file.php'); |
| 269 |
$uploaded_files = $_FILES['rag_csv']; |
| 270 |
|
| 271 |
foreach ($uploaded_files['name'] as $index => $filename) { |
| 272 |
|
| 273 |
$file_array = [ |
| 274 |
'name' => $uploaded_files['name'][$index], |
| 275 |
'type' => $uploaded_files['type'][$index], |
| 276 |
'tmp_name' => $uploaded_files['tmp_name'][$index], |
| 277 |
'error' => $uploaded_files['error'][$index], |
| 278 |
'size' => $uploaded_files['size'][$index], |
| 279 |
]; |
| 280 |
|
| 281 |
$upload = wp_handle_upload($file_array, ['test_form' => false]); |
| 282 |
|
| 283 |
if (isset($upload['error'])) { |
| 284 |
echo "<p>Error uploading: " . esc_html($filename) . "</p>"; |
| 285 |
continue; |
| 286 |
} |
| 287 |
|
| 288 |
$file_url = $upload['url']; |
| 289 |
$file_path = $upload['file']; |
| 290 |
|
| 291 |
echo "<p>Uploaded: " . esc_html($filename) . "</p>"; |
| 292 |
|
| 293 |
$handle = fopen($file_path, 'r'); // phpcs:ignore WordPress.WP.AlternativeFunctions |
| 294 |
if ($handle === false) { |
| 295 |
echo "<p style='color:red;'>Failed to open CSV file</p>"; |
| 296 |
continue; |
| 297 |
} |
| 298 |
|
| 299 |
$header = fgetcsv($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions |
| 300 |
if ($header === false) { |
| 301 |
echo "<p style='color:red;'>Empty CSV file</p>"; |
| 302 |
fclose($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions |
| 303 |
continue; |
| 304 |
} |
| 305 |
|
| 306 |
echo "<p>Columns: " . esc_html( implode(', ', $header) ) . "</p>"; |
| 307 |
|
| 308 |
global $wpdb; |
| 309 |
$table = $wpdb->prefix . "rag_documents"; |
| 310 |
$row_count = 0; |
| 311 |
$success_count = 0; |
| 312 |
|
| 313 |
while (($row = fgetcsv($handle)) !== false) { // phpcs:ignore WordPress.WP.AlternativeFunctions |
| 314 |
$row_count++; |
| 315 |
|
| 316 |
$content = ''; |
| 317 |
foreach ($header as $i => $col_name) { |
| 318 |
if (isset($row[$i])) { |
| 319 |
$content .= "$col_name: " . $row[$i] . "\n"; |
| 320 |
} |
| 321 |
} |
| 322 |
|
| 323 |
if (strlen(trim($content)) < 10) { |
| 324 |
continue; |
| 325 |
} |
| 326 |
$embedding = $this->generate_embedding($content); |
| 327 |
if (empty($embedding)) { |
| 328 |
echo "<p style='color:red;'>Failed embedding for row " . esc_html($row_count) . "</p>"; |
| 329 |
continue; |
| 330 |
} |
| 331 |
|
| 332 |
$title = !empty($row[0]) ? substr($row[0], 0, 100) : "CSV Row $row_count"; |
| 333 |
|
| 334 |
$result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 335 |
$table, [ |
| 336 |
'title' => sanitize_text_field($title), |
| 337 |
'content' => $content, |
| 338 |
'embedding' => wp_json_encode($embedding), |
| 339 |
'source_type' => 'csv', |
| 340 |
'source_url' => $file_url, |
| 341 |
'file_url' => $file_url, |
| 342 |
'status' => 'complete', |
| 343 |
'metadata' => wp_json_encode(['filename' => $filename, 'row' => $row_count]), |
| 344 |
'created_at' => current_time('mysql') |
| 345 |
]); |
| 346 |
|
| 347 |
if ($result !== false) { |
| 348 |
$success_count++; |
| 349 |
} else { |
| 350 |
echo "<p style='color:red;'>DB error row " . esc_html($row_count) . ": " . esc_html($wpdb->last_error) . "</p>"; |
| 351 |
} |
| 352 |
} |
| 353 |
|
| 354 |
fclose($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions |
| 355 |
echo "<p style='color:green;'>✓ Processed " . esc_html($success_count) . " of " . esc_html($row_count) . " rows</p>"; |
| 356 |
} |
| 357 |
|
| 358 |
echo "<h3>CSV Processing Complete!</h3>"; |
| 359 |
} |
| 360 |
public function wp_rag_process_pdf_upload() { |
| 361 |
if (empty($_FILES['rag_pdf']['name'][0])) { |
| 362 |
echo "<p>No PDF selected.</p>"; |
| 363 |
return; |
| 364 |
} |
| 365 |
|
| 366 |
require_once(ABSPATH . 'wp-admin/includes/file.php'); |
| 367 |
$uploaded_files = $_FILES['rag_pdf']; |
| 368 |
|
| 369 |
foreach ($uploaded_files['name'] as $index => $filename) { |
| 370 |
|
| 371 |
// Upload to WP Media |
| 372 |
$file_array = [ |
| 373 |
'name' => $uploaded_files['name'][$index], |
| 374 |
'type' => $uploaded_files['type'][$index], |
| 375 |
'tmp_name' => $uploaded_files['tmp_name'][$index], |
| 376 |
'error' => $uploaded_files['error'][$index], |
| 377 |
'size' => $uploaded_files['size'][$index], |
| 378 |
]; |
| 379 |
|
| 380 |
$upload = wp_handle_upload($file_array, ['test_form' => false]); |
| 381 |
|
| 382 |
if (isset($upload['error'])) { |
| 383 |
echo "<p>Error uploading: " . esc_html($filename) . "</p>"; |
| 384 |
continue; |
| 385 |
} |
| 386 |
|
| 387 |
$file_url = $upload['url']; |
| 388 |
$file_path = $upload['file']; |
| 389 |
|
| 390 |
echo "<p>Uploaded: " . esc_html($filename) . "</p>"; |
| 391 |
|
| 392 |
// Extract PDF text (uses Smalot/PdfParser) |
| 393 |
if (!class_exists('\Smalot\PdfParser\Parser')) { |
| 394 |
echo "<p>PDF parser missing! Install `smalot/pdfparser`.</p>"; |
| 395 |
return; |
| 396 |
} |
| 397 |
|
| 398 |
$parser = new \Smalot\PdfParser\Parser(); |
| 399 |
$pdf = $parser->parseFile($file_path); |
| 400 |
$text = $pdf->getText(); |
| 401 |
|
| 402 |
echo "<p>Extracted text length: " . esc_html(strlen($text)) . "</p>"; |
| 403 |
|
| 404 |
// Generate Embedding |
| 405 |
$embedding = $this->generate_embedding($text); |
| 406 |
|
| 407 |
if (empty($embedding)) { |
| 408 |
echo "<p style='color:red;'>Failed to generate embedding for: " . esc_html($filename) . "</p>"; |
| 409 |
continue; |
| 410 |
} |
| 411 |
|
| 412 |
// Save to DB |
| 413 |
global $wpdb; |
| 414 |
$table = $wpdb->prefix . "rag_documents"; |
| 415 |
|
| 416 |
$result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 417 |
$table, [ |
| 418 |
'title' => sanitize_text_field($filename), |
| 419 |
'content' => $text, |
| 420 |
'embedding' => wp_json_encode($embedding), |
| 421 |
'source_type' => 'pdf', |
| 422 |
'source_url' => $file_url, |
| 423 |
'file_url' => $file_url, |
| 424 |
'status' => 'complete', |
| 425 |
'metadata' => wp_json_encode(['size' => $uploaded_files['size'][$index]]), |
| 426 |
'created_at' => current_time('mysql') |
| 427 |
]); |
| 428 |
|
| 429 |
if ($result === false) { |
| 430 |
echo "<p style='color:red;'>Database error: " . esc_html($wpdb->last_error) . "</p>"; |
| 431 |
} else { |
| 432 |
echo "<p style='color:green;'>✓ Saved PDF embedding for: " . esc_html($filename) . " (ID: " . esc_html($wpdb->insert_id) . ")</p>"; |
| 433 |
} |
| 434 |
} |
| 435 |
|
| 436 |
echo "<h3>PDF Processing Complete!</h3>"; |
| 437 |
} |
| 438 |
|
| 439 |
// XAML processing method |
| 440 |
public function wp_rag_process_xaml_upload() { |
| 441 |
if (empty($_FILES['rag_xaml']['name'][0])) { |
| 442 |
echo "<p>No XAML selected.</p>"; |
| 443 |
return; |
| 444 |
} |
| 445 |
|
| 446 |
require_once(ABSPATH . 'wp-admin/includes/file.php'); |
| 447 |
$uploaded_files = $_FILES['rag_xaml']; |
| 448 |
|
| 449 |
foreach ($uploaded_files['name'] as $index => $filename) { |
| 450 |
|
| 451 |
$file_array = [ |
| 452 |
'name' => $uploaded_files['name'][$index], |
| 453 |
'type' => $uploaded_files['type'][$index], |
| 454 |
'tmp_name' => $uploaded_files['tmp_name'][$index], |
| 455 |
'error' => $uploaded_files['error'][$index], |
| 456 |
'size' => $uploaded_files['size'][$index], |
| 457 |
]; |
| 458 |
|
| 459 |
$upload = wp_handle_upload($file_array, [ |
| 460 |
'test_form' => false, |
| 461 |
'test_type' => false, // Bypass mime type check |
| 462 |
]); |
| 463 |
|
| 464 |
if (isset($upload['error'])) { |
| 465 |
echo "<p>Error uploading " . esc_html($filename) . ": " . esc_html($upload['error']) . "</p>"; |
| 466 |
continue; |
| 467 |
} |
| 468 |
|
| 469 |
$file_url = $upload['url']; |
| 470 |
$file_path = $upload['file']; |
| 471 |
|
| 472 |
echo "<p>Uploaded: " . esc_html($filename) . "</p>"; |
| 473 |
|
| 474 |
// Read XAML/XML content |
| 475 |
global $wp_filesystem; |
| 476 |
if ( empty( $wp_filesystem ) ) { |
| 477 |
require_once ABSPATH . '/wp-admin/includes/file.php'; |
| 478 |
WP_Filesystem(); |
| 479 |
} |
| 480 |
$xml_content = $wp_filesystem->get_contents( $file_path ); |
| 481 |
|
| 482 |
if (empty($xml_content)) { |
| 483 |
echo "<p style='color:red;'>Failed to read file or file is empty: " . esc_html($filename) . "</p>"; |
| 484 |
continue; |
| 485 |
} |
| 486 |
|
| 487 |
// Attempt to parse as XML |
| 488 |
$xml = simplexml_load_string($xml_content, 'SimpleXMLElement', LIBXML_NOCDATA); |
| 489 |
|
| 490 |
$items_to_process = []; |
| 491 |
|
| 492 |
if ($xml && isset($xml->channel->item)) { |
| 493 |
// It's likely a WordPress export (WXR) file |
| 494 |
echo "<p>Detected WordPress Export format. Extracting items...</p>"; |
| 495 |
foreach ($xml->channel->item as $item) { |
| 496 |
$title = (string)$item->title; |
| 497 |
$namespaces = $item->getNameSpaces(true); |
| 498 |
$content = ""; |
| 499 |
|
| 500 |
if (isset($namespaces['content'])) { |
| 501 |
$content = (string)$item->children($namespaces['content'])->encoded; |
| 502 |
} else { |
| 503 |
$content = (string)$item->description; |
| 504 |
} |
| 505 |
|
| 506 |
if (!empty($content)) { |
| 507 |
$items_to_process[] = [ |
| 508 |
'title' => !empty($title) ? $title : $filename, |
| 509 |
'content' => $content, |
| 510 |
'source_url' => (string)$item->link |
| 511 |
]; |
| 512 |
} |
| 513 |
} |
| 514 |
} else { |
| 515 |
// Treat as generic text/XML |
| 516 |
$items_to_process[] = [ |
| 517 |
'title' => $filename, |
| 518 |
'content' => $xml_content, |
| 519 |
'source_url' => $file_url |
| 520 |
]; |
| 521 |
} |
| 522 |
|
| 523 |
foreach ($items_to_process as $item_data) { |
| 524 |
$clean_content = $this->clean_rag_content($item_data['content']); |
| 525 |
|
| 526 |
if (empty($clean_content) || strlen($clean_content) < 20) { |
| 527 |
continue; |
| 528 |
} |
| 529 |
|
| 530 |
// Generate Embedding |
| 531 |
$embedding = $this->generate_embedding($clean_content); |
| 532 |
|
| 533 |
if (empty($embedding)) { |
| 534 |
echo "<p style='color:red;'>Failed to generate embedding for item: " . esc_html($item_data['title']) . "</p>"; |
| 535 |
continue; |
| 536 |
} |
| 537 |
|
| 538 |
// Save to DB |
| 539 |
global $wpdb; |
| 540 |
$table = $wpdb->prefix . "rag_documents"; |
| 541 |
|
| 542 |
$result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 543 |
$table, [ |
| 544 |
'title' => sanitize_text_field($item_data['title']), |
| 545 |
'content' => $clean_content, |
| 546 |
'embedding' => wp_json_encode($embedding), |
| 547 |
'source_type' => 'xaml', |
| 548 |
'source_url' => $item_data['source_url'] ? $item_data['source_url'] : $file_url, |
| 549 |
'file_url' => $file_url, |
| 550 |
'status' => 'complete', |
| 551 |
'metadata' => wp_json_encode(['size' => strlen($clean_content)]), |
| 552 |
'created_at' => current_time('mysql') |
| 553 |
]); |
| 554 |
|
| 555 |
if ($result === false) { |
| 556 |
echo "<p style='color:red;'>Database error for item " . esc_html($item_data['title']) . ": " . esc_html($wpdb->last_error) . "</p>"; |
| 557 |
} else { |
| 558 |
echo "<p style='color:green;'>✓ Saved embedding for: " . esc_html($item_data['title']) . " (ID: " . esc_html($wpdb->insert_id) . ")</p>"; |
| 559 |
} |
| 560 |
} |
| 561 |
} |
| 562 |
|
| 563 |
echo "<h3>XAML Processing Complete!</h3>"; |
| 564 |
} |
| 565 |
|
| 566 |
public function wp_rag_embed_all_documents() |
| 567 |
{ |
| 568 |
$apiKey = get_option('open_ai_api_key'); // Replace with option if needed |
| 569 |
global $wpdb; |
| 570 |
|
| 571 |
$posts = get_posts([ |
| 572 |
'post_type' => ['post', 'page'], |
| 573 |
'posts_per_page' => -1 |
| 574 |
]); |
| 575 |
|
| 576 |
echo "<ul>"; |
| 577 |
|
| 578 |
foreach ($posts as $p) { |
| 579 |
$content = wp_strip_all_tags($p->post_content); |
| 580 |
if (strlen($content) < 20) continue; |
| 581 |
|
| 582 |
$embedding = $this->wp_rag_create_embedding($content, $apiKey); |
| 583 |
|
| 584 |
$wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 585 |
$wpdb->prefix . "rag_documents", |
| 586 |
[ |
| 587 |
"title" => $p->post_title, |
| 588 |
"content" => $content, |
| 589 |
"embedding" => wp_json_encode($embedding) |
| 590 |
] |
| 591 |
); |
| 592 |
|
| 593 |
echo "<li>Embedded: " . esc_html($p->post_title) . "</li>"; |
| 594 |
flush(); |
| 595 |
} |
| 596 |
|
| 597 |
echo "</ul>"; |
| 598 |
echo "<strong>Completed!</strong>"; |
| 599 |
} |
| 600 |
|
| 601 |
public function wp_rag_embed_all_sources() |
| 602 |
{ |
| 603 |
// $apiKey = get_option('open_ai_api_key'); |
| 604 |
global $wpdb; |
| 605 |
|
| 606 |
$post_types = []; |
| 607 |
if (get_option('rag_embed_pages') == '1') { |
| 608 |
$post_types[] = 'page'; |
| 609 |
} |
| 610 |
if (get_option('rag_embed_posts') == '1') { |
| 611 |
$post_types[] = 'post'; |
| 612 |
} |
| 613 |
|
| 614 |
$cpts = get_option('rag_embed_cpts', []); |
| 615 |
if (!empty($cpts) && is_array($cpts)) { |
| 616 |
$post_types = array_merge($post_types, $cpts); |
| 617 |
} |
| 618 |
|
| 619 |
$table = $wpdb->prefix . "rag_documents"; |
| 620 |
$updated_count = 0; |
| 621 |
$inserted_count = 0; |
| 622 |
$skipped_count = 0; |
| 623 |
|
| 624 |
echo "<ul>"; |
| 625 |
|
| 626 |
if (!empty($post_types)) { |
| 627 |
$posts = get_posts([ |
| 628 |
'post_type' => $post_types, |
| 629 |
'posts_per_page' => -1, |
| 630 |
'post_status' => 'publish' |
| 631 |
]); |
| 632 |
|
| 633 |
foreach ($posts as $p) { |
| 634 |
$title = $p->post_title; |
| 635 |
$content = "Title: " . $title . "\n"; |
| 636 |
$content .= "Date: " . $p->post_date . "\n"; |
| 637 |
|
| 638 |
$main_content = strip_shortcodes($p->post_content); |
| 639 |
$main_content = wp_strip_all_tags($main_content); |
| 640 |
$content .= $main_content; |
| 641 |
|
| 642 |
// Specific handling for WooCommerce Products |
| 643 |
if ($p->post_type === 'product' && class_exists('WC_Product') && function_exists('wc_get_product')) { |
| 644 |
$_product = wc_get_product($p->ID); |
| 645 |
if ($_product) { |
| 646 |
$price = $_product->get_price(); |
| 647 |
$currency = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol() : '$'; |
| 648 |
$content .= "\nPrice: " . $currency . $price; |
| 649 |
|
| 650 |
// Add description if main content is empty |
| 651 |
if (empty(trim($main_content)) && method_exists($_product, 'get_short_description')) { |
| 652 |
$content .= "\nDescription: " . wp_strip_all_tags($_product->get_short_description()); |
| 653 |
} |
| 654 |
} |
| 655 |
} |
| 656 |
|
| 657 |
if (strlen(trim($content)) < 20) { |
| 658 |
$skipped_count++; |
| 659 |
continue; |
| 660 |
} |
| 661 |
|
| 662 |
$embedding = $this->generate_embedding($content); |
| 663 |
|
| 664 |
if (empty($embedding)) { |
| 665 |
echo "<li style='color:red;'>Failed to generate embedding for: " . esc_html($p->post_title) . "</li>"; |
| 666 |
continue; |
| 667 |
} |
| 668 |
|
| 669 |
$table = $wpdb->prefix . "rag_documents"; |
| 670 |
|
| 671 |
// Check if this post already exists in the database |
| 672 |
$existing = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 673 |
$wpdb->prepare( |
| 674 |
"SELECT id FROM $table WHERE metadata LIKE %s AND source_type = %s", |
| 675 |
'%"post_id":' . $p->ID . '%', |
| 676 |
$p->post_type |
| 677 |
)); |
| 678 |
|
| 679 |
$data = [ |
| 680 |
"title" => $p->post_title, |
| 681 |
"content" => $content, |
| 682 |
"embedding" => wp_json_encode($embedding), |
| 683 |
"source_type" => $p->post_type, |
| 684 |
"source_url" => get_permalink($p->ID), |
| 685 |
"file_url" => get_permalink($p->ID), |
| 686 |
"status" => 'complete', |
| 687 |
"metadata" => wp_json_encode(['post_id' => $p->ID]), |
| 688 |
"created_at" => current_time('mysql') |
| 689 |
]; |
| 690 |
|
| 691 |
if ($existing) { |
| 692 |
// Update existing record |
| 693 |
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 694 |
$table, |
| 695 |
$data, |
| 696 |
['id' => $existing->id] |
| 697 |
); |
| 698 |
echo "<li style='color:blue;'>✓ Updated: " . esc_html($p->post_title) . " (" . esc_html($p->post_type) . ")</li>"; |
| 699 |
$updated_count++; |
| 700 |
} else { |
| 701 |
// Insert new record |
| 702 |
$wpdb->insert($table, $data); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 703 |
echo "<li style='color:green;'>✓ Embedded: " . esc_html($p->post_title) . " (" . esc_html($p->post_type) . ")</li>"; |
| 704 |
$inserted_count++; |
| 705 |
} |
| 706 |
|
| 707 |
if (function_exists('flush')) { |
| 708 |
@flush(); |
| 709 |
} |
| 710 |
if (function_exists('ob_flush')) { |
| 711 |
@ob_flush(); |
| 712 |
} |
| 713 |
} |
| 714 |
} |
| 715 |
|
| 716 |
// Simple Text Responses Embedding |
| 717 |
if (get_option('rag_embed_str') == '1') { |
| 718 |
$str_table = $wpdb->prefix . 'wpbot_response'; |
| 719 |
$str_results = $wpdb->get_results("SELECT * FROM $str_table"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 720 |
|
| 721 |
if (!empty($str_results)) { |
| 722 |
foreach ($str_results as $str) { |
| 723 |
$content = "Query: " . $str->query . "\n"; |
| 724 |
$content .= "Response: " . wp_strip_all_tags($str->response) . "\n"; |
| 725 |
if (!empty($str->keyword)) { |
| 726 |
$content .= "Keywords: " . $str->keyword; |
| 727 |
} |
| 728 |
|
| 729 |
if (strlen(trim($content)) < 10) { |
| 730 |
$skipped_count++; |
| 731 |
continue; |
| 732 |
} |
| 733 |
|
| 734 |
$embedding = $this->generate_embedding($content); |
| 735 |
if (empty($embedding)) { |
| 736 |
echo "<li style='color:red;'>Failed to generate embedding for STR: " . esc_html($str->query) . "</li>"; |
| 737 |
continue; |
| 738 |
} |
| 739 |
|
| 740 |
// Check if this STR already exists in the RAG database |
| 741 |
$existing = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 742 |
$wpdb->prepare( |
| 743 |
"SELECT id FROM $table WHERE metadata LIKE %s AND source_type = %s", |
| 744 |
'%"str_id":' . $str->id . '%', |
| 745 |
'str' |
| 746 |
)); |
| 747 |
|
| 748 |
$data = [ |
| 749 |
"title" => $str->query, |
| 750 |
"content" => $content, |
| 751 |
"embedding" => wp_json_encode($embedding), |
| 752 |
"source_type" => 'str', |
| 753 |
"source_url" => admin_url('admin.php?page=simple-text-response&action=edit&query=' . $str->id), |
| 754 |
"file_url" => '', |
| 755 |
"status" => 'complete', |
| 756 |
"metadata" => wp_json_encode(['str_id' => $str->id]), |
| 757 |
"created_at" => current_time('mysql') |
| 758 |
]; |
| 759 |
|
| 760 |
if ($existing) { |
| 761 |
$wpdb->update($table, $data, ['id' => $existing->id]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 762 |
echo "<li style='color:blue;'>✓ Updated STR: " . esc_html($str->query) . "</li>"; |
| 763 |
$updated_count++; |
| 764 |
} else { |
| 765 |
$wpdb->insert($table, $data); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 766 |
echo "<li style='color:green;'>✓ Embedded STR: " . esc_html($str->query) . "</li>"; |
| 767 |
$inserted_count++; |
| 768 |
} |
| 769 |
|
| 770 |
if (function_exists('flush')) { @flush(); } |
| 771 |
if (function_exists('ob_flush')) { @ob_flush(); } |
| 772 |
} |
| 773 |
} |
| 774 |
} |
| 775 |
|
| 776 |
echo "</ul>"; |
| 777 |
echo "<h3>All Selected Sources Processed!</h3>"; |
| 778 |
echo "<p><strong>Summary:</strong></p>"; |
| 779 |
echo "<ul>"; |
| 780 |
echo "<li>New entries created: <strong>" . esc_html($inserted_count) . "</strong></li>"; |
| 781 |
echo "<li>Existing entries updated: <strong style='color:blue;'>" . esc_html($updated_count) . "</strong></li>"; |
| 782 |
echo "<li>Skipped (too short): <strong>" . esc_html($skipped_count) . "</strong></li>"; |
| 783 |
echo "</ul>"; |
| 784 |
} |
| 785 |
public function wp_rag_create_embedding($text, $apiKey) |
| 786 |
{ |
| 787 |
|
| 788 |
$response = $this->generate_embedding($text); |
| 789 |
return $response; |
| 790 |
} |
| 791 |
public function ajax_rag_manual_sync() { |
| 792 |
|
| 793 |
check_ajax_referer('wp_chatbot', 'nonce'); |
| 794 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 795 |
wp_die( 'Unauthorized access' ); |
| 796 |
} |
| 797 |
|
| 798 |
$doc_id = isset($_POST['id']) ? intval($_POST['id']) : 0; |
| 799 |
if (!$doc_id) { |
| 800 |
wp_send_json_error(['message' => 'Invalid document ID']); |
| 801 |
} |
| 802 |
|
| 803 |
global $wpdb; |
| 804 |
$table = $wpdb->prefix . 'rag_documents'; |
| 805 |
$doc = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $doc_id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 806 |
|
| 807 |
if (!$doc) { |
| 808 |
wp_send_json_error(['message' => 'Document not found']); |
| 809 |
} |
| 810 |
|
| 811 |
// Try to extract post/product info |
| 812 |
$post_id = 0; |
| 813 |
if (!empty($doc->metadata)) { |
| 814 |
$metadata = json_decode($doc->metadata, true); |
| 815 |
if (isset($metadata['post_id'])) { |
| 816 |
$post_id = intval($metadata['post_id']); |
| 817 |
} |
| 818 |
} |
| 819 |
|
| 820 |
if (!$post_id && ($doc->source_type === 'page' || $doc->source_type === 'post' || $doc->source_type === 'xaml')) { |
| 821 |
$post_id = url_to_postid($doc->source_url); |
| 822 |
} |
| 823 |
|
| 824 |
if (!$post_id) { |
| 825 |
wp_send_json_error(['message' => 'Could not determine source post for manual sync']); |
| 826 |
} |
| 827 |
|
| 828 |
$result = $this->wp_rag_sync_post($post_id, true); |
| 829 |
|
| 830 |
if (is_wp_error($result)) { |
| 831 |
wp_send_json_error(['message' => $result->get_error_message()]); |
| 832 |
} |
| 833 |
|
| 834 |
wp_send_json_success(['message' => 'Document synced successfully!']); |
| 835 |
} |
| 836 |
public function qcld_rag_delete_document_callback() { |
| 837 |
check_ajax_referer('wp_chatbot', 'nonce'); |
| 838 |
if (!current_user_can('manage_options')) { |
| 839 |
wp_send_json_error('Unauthorized'); |
| 840 |
} |
| 841 |
|
| 842 |
global $wpdb; |
| 843 |
$id = intval($_POST['id']); |
| 844 |
$table_name = $wpdb->prefix . 'rag_documents'; |
| 845 |
|
| 846 |
$deleted = $wpdb->delete($table_name, array('id' => $id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 847 |
|
| 848 |
if ($deleted) { |
| 849 |
wp_send_json_success('Document deleted successfully.'); |
| 850 |
} else { |
| 851 |
wp_send_json_error('Failed to delete document.'); |
| 852 |
} |
| 853 |
} |
| 854 |
public function qcld_rag_bulk_delete_documents_callback() { |
| 855 |
check_ajax_referer('wp_chatbot', 'nonce'); |
| 856 |
if (!current_user_can('manage_options')) { |
| 857 |
wp_send_json_error('Unauthorized'); |
| 858 |
} |
| 859 |
|
| 860 |
if (empty($_POST['ids']) || !is_array($_POST['ids'])) { |
| 861 |
wp_send_json_error('No documents selected.'); |
| 862 |
} |
| 863 |
|
| 864 |
global $wpdb; |
| 865 |
$ids = array_map('intval', $_POST['ids']); |
| 866 |
$table_name = $wpdb->prefix . 'rag_documents'; |
| 867 |
|
| 868 |
$ids_string = implode(',', $ids); |
| 869 |
$deleted = $wpdb->query("DELETE FROM $table_name WHERE id IN ($ids_string)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 870 |
|
| 871 |
if ($deleted !== false) { |
| 872 |
wp_send_json_success('Selected documents deleted successfully.'); |
| 873 |
} else { |
| 874 |
wp_send_json_error('Failed to delete selected documents.'); |
| 875 |
} |
| 876 |
} |
| 877 |
public function qcld_rag_delete_all_documents_callback() { |
| 878 |
check_ajax_referer('wp_chatbot', 'nonce'); |
| 879 |
if (!current_user_can('manage_options')) { |
| 880 |
wp_send_json_error('Unauthorized'); |
| 881 |
} |
| 882 |
|
| 883 |
global $wpdb; |
| 884 |
$table_name = $wpdb->prefix . 'rag_documents'; |
| 885 |
|
| 886 |
$deleted = $wpdb->query("TRUNCATE TABLE $table_name"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 887 |
|
| 888 |
// Some DBs might not support TRUNCATE on tables with foreign keys or other constraints, |
| 889 |
// though rag_documents is likely simple. Fallback to DELETE. |
| 890 |
if ($deleted === false) { |
| 891 |
$deleted = $wpdb->query("DELETE FROM $table_name"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 892 |
} |
| 893 |
|
| 894 |
if ($deleted !== false) { |
| 895 |
wp_send_json_success('All documents deleted successfully.'); |
| 896 |
} else { |
| 897 |
wp_send_json_error('Failed to delete all documents.'); |
| 898 |
} |
| 899 |
} |
| 900 |
public function qcld_rag_get_document_callback() { |
| 901 |
check_ajax_referer('wp_chatbot', 'nonce'); |
| 902 |
if (!current_user_can('manage_options')) { |
| 903 |
wp_send_json_error('Unauthorized'); |
| 904 |
} |
| 905 |
|
| 906 |
global $wpdb; |
| 907 |
$id = intval($_POST['id']); |
| 908 |
$table_name = $wpdb->prefix . 'rag_documents'; |
| 909 |
|
| 910 |
$document = $wpdb->get_row($wpdb->prepare("SELECT id, title, content FROM $table_name WHERE id = %d", $id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 911 |
|
| 912 |
if ($document) { |
| 913 |
wp_send_json_success($document); |
| 914 |
} else { |
| 915 |
wp_send_json_error('Document not found.'); |
| 916 |
} |
| 917 |
} |
| 918 |
public function qcld_rag_update_document_callback() { |
| 919 |
check_ajax_referer('wp_chatbot', 'nonce'); |
| 920 |
if (!current_user_can('manage_options')) { |
| 921 |
wp_send_json_error('Unauthorized'); |
| 922 |
} |
| 923 |
|
| 924 |
global $wpdb; |
| 925 |
$id = intval(wp_unslash($_POST['id'])); |
| 926 |
$title = sanitize_text_field(wp_unslash($_POST['title'])); |
| 927 |
$content = sanitize_textarea_field(wp_unslash($_POST['content'])); |
| 928 |
$table_name = $wpdb->prefix . 'rag_documents'; |
| 929 |
|
| 930 |
// Re-generate embedding if content changed |
| 931 |
$old_content = $wpdb->get_var($wpdb->prepare("SELECT content FROM $table_name WHERE id = %d", $id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 932 |
|
| 933 |
$update_data = array( |
| 934 |
'title' => $title, |
| 935 |
'content' => $content, |
| 936 |
'status' => 'complete' |
| 937 |
); |
| 938 |
|
| 939 |
if ($old_content !== $content) { |
| 940 |
$embedding = $this->generate_embedding($content); |
| 941 |
if (!empty($embedding)) { |
| 942 |
$update_data['embedding'] = wp_json_encode($embedding); |
| 943 |
} else { |
| 944 |
$update_data['status'] = 'error'; |
| 945 |
} |
| 946 |
} |
| 947 |
|
| 948 |
$updated = $wpdb->update($table_name, $update_data, array('id' => $id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 949 |
|
| 950 |
if ($updated !== false) { |
| 951 |
wp_send_json_success('Document updated successfully.'); |
| 952 |
} else { |
| 953 |
wp_send_json_error('Failed to update document.'); |
| 954 |
} |
| 955 |
} |
| 956 |
public function wp_rag_sync_post($post_id, $force = false) { |
| 957 |
$post = get_post($post_id); |
| 958 |
if (!$post) return new WP_Error('invalid_post', 'Post not found'); |
| 959 |
|
| 960 |
$title = $post->post_title; |
| 961 |
$url = get_permalink($post_id); |
| 962 |
$content = "Title: " . $title . "\n"; |
| 963 |
$content .= "Date: " . $post->post_date . "\n"; |
| 964 |
|
| 965 |
$main_content = strip_shortcodes($post->post_content); |
| 966 |
$main_content = wp_strip_all_tags($main_content); |
| 967 |
$content .= $main_content; |
| 968 |
|
| 969 |
// Specific handling for WooCommerce Products |
| 970 |
if ($post->post_type === 'product' && class_exists('WC_Product') && function_exists('wc_get_product')) { |
| 971 |
$_product = wc_get_product($post_id); |
| 972 |
if ($_product) { |
| 973 |
$price = $_product->get_price(); |
| 974 |
$currency = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol() : '$'; |
| 975 |
$content .= "\nPrice: " . $currency . $price; |
| 976 |
|
| 977 |
// Add description if main content is empty (sometimes WC uses short description) |
| 978 |
if (empty($main_content) && method_exists($_product, 'get_short_description')) { |
| 979 |
$content .= "\nDescription: " . wp_strip_all_tags($_product->get_short_description()); |
| 980 |
} |
| 981 |
} |
| 982 |
} |
| 983 |
|
| 984 |
if (empty(trim($main_content)) && !($post->post_type === 'product')) { |
| 985 |
return new WP_Error('empty_content', 'No content found to embed'); |
| 986 |
} |
| 987 |
|
| 988 |
// Generate Embedding |
| 989 |
$embedding = $this->generate_embedding($content); |
| 990 |
if (empty($embedding)) { |
| 991 |
return new WP_Error('embedding_failed', 'Failed to generate embedding'); |
| 992 |
} |
| 993 |
|
| 994 |
global $wpdb; |
| 995 |
$table = $wpdb->prefix . "rag_documents"; |
| 996 |
|
| 997 |
// Check if it already exists (by source_url or custom metadata if we had it) |
| 998 |
$existing = $wpdb->get_row($wpdb->prepare("SELECT id FROM $table WHERE source_url = %s", $url)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 999 |
|
| 1000 |
if ($existing) { |
| 1001 |
$result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 1002 |
$table, [ |
| 1003 |
'title' => sanitize_text_field($title), |
| 1004 |
'content' => $content, |
| 1005 |
'embedding' => wp_json_encode($embedding), |
| 1006 |
'status' => 'complete', |
| 1007 |
'created_at' => current_time('mysql') |
| 1008 |
], ['id' => $existing->id]); |
| 1009 |
} else { |
| 1010 |
$result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 1011 |
$table, [ |
| 1012 |
'title' => sanitize_text_field($title), |
| 1013 |
'content' => $content, |
| 1014 |
'embedding' => wp_json_encode($embedding), |
| 1015 |
'source_type' => ($post->post_type === 'page' || $post->post_type === 'post') ? $post->post_type : 'xaml', |
| 1016 |
'source_url' => $url, |
| 1017 |
'file_url' => $url, |
| 1018 |
'status' => 'complete', |
| 1019 |
'metadata' => wp_json_encode(['post_id' => $post_id, 'post_type' => $post->post_type]), |
| 1020 |
'created_at' => current_time('mysql') |
| 1021 |
]); |
| 1022 |
} |
| 1023 |
|
| 1024 |
return $result; |
| 1025 |
} |
| 1026 |
public function wp_rag_handle_auto_sync_hook($post_id, $post, $update) { |
| 1027 |
// Only run if auto-sync is enabled |
| 1028 |
if (get_option('rag_auto_sync_enabled') != '1') { |
| 1029 |
return; |
| 1030 |
} |
| 1031 |
|
| 1032 |
// Avoid autosaves and revisions |
| 1033 |
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; |
| 1034 |
if (wp_is_post_revision($post_id)) return; |
| 1035 |
if ($post->post_status != 'publish') return; |
| 1036 |
|
| 1037 |
// Check if post type is enabled in general embedding settings |
| 1038 |
$is_allowed = false; |
| 1039 |
if ($post->post_type === 'page' && get_option('rag_embed_pages') == '1') { |
| 1040 |
$is_allowed = true; |
| 1041 |
} elseif ($post->post_type === 'post' && get_option('rag_embed_posts') == '1') { |
| 1042 |
$is_allowed = true; |
| 1043 |
} else { |
| 1044 |
$cpts = get_option('rag_embed_cpts', []); |
| 1045 |
if (is_array($cpts) && in_array($post->post_type, $cpts)) { |
| 1046 |
$is_allowed = true; |
| 1047 |
} |
| 1048 |
} |
| 1049 |
|
| 1050 |
if (!$is_allowed) { |
| 1051 |
return; |
| 1052 |
} |
| 1053 |
|
| 1054 |
$this->wp_rag_sync_post($post_id); |
| 1055 |
} |
| 1056 |
|
| 1057 |
public function clean_rag_content($text) { |
| 1058 |
if (empty($text)) return ""; |
| 1059 |
|
| 1060 |
// Remove WordPress block comments like <!-- wp:paragraph --> |
| 1061 |
$text = preg_replace('/<!--\s*\/?[a-z0-9_-]+:[a-z0-9_-]+\s*({.*?})?\s*-->/s', '', $text); |
| 1062 |
|
| 1063 |
// Remove generic HTML comments |
| 1064 |
$text = preg_replace('/<!--(.*?)-->/s', '', $text); |
| 1065 |
|
| 1066 |
// Strip HTML tags |
| 1067 |
$text = wp_strip_all_tags($text); |
| 1068 |
|
| 1069 |
// Decode HTML entities |
| 1070 |
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); |
| 1071 |
|
| 1072 |
// Normalize whitespace |
| 1073 |
$text = preg_replace('/\s+/', ' ', $text); |
| 1074 |
|
| 1075 |
return trim($text); |
| 1076 |
} |
| 1077 |
|
| 1078 |
public function run_rag_search($user_query, $top_k = 3) { |
| 1079 |
global $wpdb; |
| 1080 |
$table = $wpdb->prefix . "rag_documents"; |
| 1081 |
|
| 1082 |
// Get all embeddings and texts |
| 1083 |
$rows = $wpdb->get_results("SELECT content, embedding FROM $table WHERE status = 'complete'", ARRAY_A); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 1084 |
|
| 1085 |
if (empty($rows)) { |
| 1086 |
return "No knowledge base found."; |
| 1087 |
} |
| 1088 |
|
| 1089 |
// Step 1: Get embedding for user query |
| 1090 |
$query_vector = $this->generate_embedding($user_query); |
| 1091 |
|
| 1092 |
if (empty($query_vector)) { |
| 1093 |
return ""; |
| 1094 |
} |
| 1095 |
|
| 1096 |
// Step 2: Compute cosine similarity |
| 1097 |
$ranked = []; |
| 1098 |
foreach ($rows as $row) { |
| 1099 |
$doc_embedding = json_decode($row['embedding'], true); |
| 1100 |
if (!is_array($doc_embedding) || empty($doc_embedding)) { |
| 1101 |
continue; |
| 1102 |
} |
| 1103 |
$sim = $this->cosine_similarity($query_vector, $doc_embedding); |
| 1104 |
$ranked[] = ["score" => $sim, "text" => $row['content']]; |
| 1105 |
} |
| 1106 |
|
| 1107 |
// Step 3: Sort by similarity |
| 1108 |
usort($ranked, function ($a, $b) { |
| 1109 |
return $a['score'] < $b['score'] ? 1 : -1; |
| 1110 |
}); |
| 1111 |
|
| 1112 |
// Select top k documents |
| 1113 |
$top_docs = array_slice($ranked, 0, $top_k); |
| 1114 |
|
| 1115 |
$context = ""; |
| 1116 |
foreach ($top_docs as $doc) { |
| 1117 |
$context .= $doc["text"] . "\n\n"; |
| 1118 |
} |
| 1119 |
|
| 1120 |
return trim($context); |
| 1121 |
} |
| 1122 |
|
| 1123 |
private function cosine_similarity($vecA, $vecB) { |
| 1124 |
if (!is_array($vecA) || !is_array($vecB) || count($vecA) !== count($vecB)) { |
| 1125 |
return 0.0; |
| 1126 |
} |
| 1127 |
$dot = 0.0; |
| 1128 |
$normA = 0.0; |
| 1129 |
$normB = 0.0; |
| 1130 |
|
| 1131 |
$len = count($vecA); |
| 1132 |
for ($i = 0; $i < $len; $i++) { |
| 1133 |
$dot += $vecA[$i] * $vecB[$i]; |
| 1134 |
$normA += $vecA[$i] ** 2; |
| 1135 |
$normB += $vecB[$i] ** 2; |
| 1136 |
} |
| 1137 |
|
| 1138 |
if ($normA == 0 || $normB == 0) { |
| 1139 |
return 0.0; |
| 1140 |
} |
| 1141 |
|
| 1142 |
return $dot / (sqrt($normA) * sqrt($normB)); |
| 1143 |
} |
| 1144 |
|
| 1145 |
public function ajax_qcld_rag_get_embed_queue() { |
| 1146 |
check_ajax_referer('wp_chatbot', 'nonce'); |
| 1147 |
if (!current_user_can('manage_options')) { |
| 1148 |
wp_send_json_error('Unauthorized'); |
| 1149 |
} |
| 1150 |
|
| 1151 |
global $wpdb; |
| 1152 |
$queue = []; |
| 1153 |
|
| 1154 |
// Posts, Pages, CPTs |
| 1155 |
$post_types = []; |
| 1156 |
if (get_option('rag_embed_pages') == '1') { |
| 1157 |
$post_types[] = 'page'; |
| 1158 |
} |
| 1159 |
if (get_option('rag_embed_posts') == '1') { |
| 1160 |
$post_types[] = 'post'; |
| 1161 |
} |
| 1162 |
|
| 1163 |
$cpts = get_option('rag_embed_cpts', []); |
| 1164 |
if (!empty($cpts) && is_array($cpts)) { |
| 1165 |
$post_types = array_merge($post_types, $cpts); |
| 1166 |
} |
| 1167 |
|
| 1168 |
if (!empty($post_types)) { |
| 1169 |
$posts = get_posts([ |
| 1170 |
'post_type' => $post_types, |
| 1171 |
'posts_per_page' => -1, |
| 1172 |
'post_status' => 'publish', |
| 1173 |
'fields' => 'ids' |
| 1174 |
]); |
| 1175 |
foreach ($posts as $post_id) { |
| 1176 |
$queue[] = ['id' => $post_id, 'type' => 'post']; |
| 1177 |
} |
| 1178 |
} |
| 1179 |
|
| 1180 |
// Simple Text Responses |
| 1181 |
if (get_option('rag_embed_str') == '1') { |
| 1182 |
$str_ids = $wpdb->get_col("SELECT id FROM {$wpdb->prefix}wpbot_response"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 1183 |
foreach ($str_ids as $str_id) { |
| 1184 |
$queue[] = ['id' => $str_id, 'type' => 'str']; |
| 1185 |
} |
| 1186 |
} |
| 1187 |
|
| 1188 |
wp_send_json_success($queue); |
| 1189 |
} |
| 1190 |
|
| 1191 |
public function ajax_qcld_rag_process_item() { |
| 1192 |
check_ajax_referer('wp_chatbot', 'nonce'); |
| 1193 |
if (!current_user_can('manage_options')) { |
| 1194 |
wp_send_json_error('Unauthorized'); |
| 1195 |
} |
| 1196 |
|
| 1197 |
$id = intval($_POST['item_id']); |
| 1198 |
$type = sanitize_text_field($_POST['item_type']); |
| 1199 |
|
| 1200 |
if ($type === 'post') { |
| 1201 |
$p = get_post($id); |
| 1202 |
if (!$p) { |
| 1203 |
wp_send_json_error('Post not found'); |
| 1204 |
} |
| 1205 |
|
| 1206 |
global $wpdb; |
| 1207 |
$table = $wpdb->prefix . "rag_documents"; |
| 1208 |
$apiKey = get_option('open_ai_api_key'); |
| 1209 |
|
| 1210 |
$title = $p->post_title; |
| 1211 |
$content = "Title: " . $title . "\n"; |
| 1212 |
|
| 1213 |
|
| 1214 |
$main_content = strip_shortcodes($p->post_content); |
| 1215 |
$main_content = wp_strip_all_tags($main_content); |
| 1216 |
$content .= $main_content; |
| 1217 |
|
| 1218 |
// Add Post Meta Context |
| 1219 |
$content .= $this->get_post_meta_context($id); |
| 1220 |
|
| 1221 |
if ($p->post_type === 'product' && class_exists('WC_Product')) { |
| 1222 |
$_product = wc_get_product($p->ID); |
| 1223 |
if ($_product) { |
| 1224 |
$price = $_product->get_price(); |
| 1225 |
$currency = get_woocommerce_currency_symbol(); |
| 1226 |
$content .= "\nPrice: " . $currency . $price; |
| 1227 |
if (empty(trim($main_content))) { |
| 1228 |
$content .= "\nDescription: " . wp_strip_all_tags($_product->get_short_description()); |
| 1229 |
} |
| 1230 |
$content .= "\nProduct Link: " . get_permalink($p->ID); |
| 1231 |
$content .= "\nProduct ID: " . $p->ID; |
| 1232 |
|
| 1233 |
} |
| 1234 |
} |
| 1235 |
|
| 1236 |
if (strlen(trim($content)) < 20) { |
| 1237 |
wp_send_json_success(['status' => 'skipped', 'title' => $title]); |
| 1238 |
} |
| 1239 |
|
| 1240 |
$embedding = $this->wp_rag_create_embedding($content, $apiKey); |
| 1241 |
if (empty($embedding) || is_wp_error($embedding)) { |
| 1242 |
$error_msg = is_wp_error($embedding) ? $embedding->get_error_message() : 'Failed to generate embedding'; |
| 1243 |
wp_send_json_error($error_msg); |
| 1244 |
} |
| 1245 |
|
| 1246 |
$existing = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 1247 |
$wpdb->prepare( |
| 1248 |
"SELECT id FROM $table WHERE metadata LIKE %s AND source_type = %s", |
| 1249 |
'%"post_id":' . $p->ID . '%', |
| 1250 |
$p->post_type |
| 1251 |
)); |
| 1252 |
|
| 1253 |
$data = [ |
| 1254 |
"title" => $p->post_title, |
| 1255 |
"content" => $content, |
| 1256 |
"embedding" => wp_json_encode($embedding), |
| 1257 |
"source_type" => $p->post_type, |
| 1258 |
"source_url" => get_permalink($p->ID), |
| 1259 |
"file_url" => get_permalink($p->ID), |
| 1260 |
"status" => 'complete', |
| 1261 |
"metadata" => wp_json_encode(['post_id' => $p->ID]), |
| 1262 |
"created_at" => current_time('mysql') |
| 1263 |
]; |
| 1264 |
|
| 1265 |
if ($existing) { |
| 1266 |
$wpdb->update($table, $data, ['id' => $existing->id]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 1267 |
wp_send_json_success(['status' => 'updated', 'title' => $title]); |
| 1268 |
} else { |
| 1269 |
$wpdb->insert($table, $data); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 1270 |
wp_send_json_success(['status' => 'inserted', 'title' => $title]); |
| 1271 |
} |
| 1272 |
|
| 1273 |
} elseif ($type === 'str') { |
| 1274 |
global $wpdb; |
| 1275 |
$str = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}wpbot_response WHERE id = %d", $id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 1276 |
if (!$str) { |
| 1277 |
wp_send_json_error('STR not found'); |
| 1278 |
} |
| 1279 |
|
| 1280 |
$content = "Query: " . $str->query . "\n"; |
| 1281 |
$content .= "Response: " . wp_strip_all_tags($str->response) . "\n"; |
| 1282 |
if (!empty($str->keyword)) { |
| 1283 |
$content .= "Keywords: " . $str->keyword . "\n"; |
| 1284 |
} |
| 1285 |
if (!empty($str->intent)) { |
| 1286 |
$content .= "Intent: " . $str->intent . "\n"; |
| 1287 |
} |
| 1288 |
|
| 1289 |
if (strlen(trim($content)) < 20) { |
| 1290 |
wp_send_json_error('No content found to embed'); |
| 1291 |
} |
| 1292 |
|
| 1293 |
$embedding = $this->generate_embedding($content); |
| 1294 |
|
| 1295 |
if (empty($embedding) || is_wp_error($embedding)) { |
| 1296 |
wp_send_json_error('Failed to generate embedding'); |
| 1297 |
} |
| 1298 |
|
| 1299 |
$table = $wpdb->prefix . "rag_documents"; |
| 1300 |
|
| 1301 |
$existing = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 1302 |
$wpdb->prepare( |
| 1303 |
"SELECT id FROM $table WHERE metadata LIKE %s AND source_type = %s", |
| 1304 |
'%"str_id":' . $str->id . '%', |
| 1305 |
'str' |
| 1306 |
)); |
| 1307 |
|
| 1308 |
$data = [ |
| 1309 |
"title" => $str->query, |
| 1310 |
"content" => $content, |
| 1311 |
"embedding" => wp_json_encode($embedding), |
| 1312 |
"source_type" => 'str', |
| 1313 |
"source_url" => '', |
| 1314 |
"file_url" => '', |
| 1315 |
"status" => 'complete', |
| 1316 |
"metadata" => wp_json_encode(['str_id' => $str->id]), |
| 1317 |
"created_at" => current_time('mysql') |
| 1318 |
]; |
| 1319 |
|
| 1320 |
if ($existing) { |
| 1321 |
$wpdb->update($table, $data, ['id' => $existing->id]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 1322 |
} else { |
| 1323 |
$wpdb->insert($table, $data); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 1324 |
} |
| 1325 |
wp_send_json_success(['status' => 'processed', 'title' => 'Simple Text Response ID ' . $id]); |
| 1326 |
} |
| 1327 |
|
| 1328 |
wp_send_json_error('Invalid item type'); |
| 1329 |
} |
| 1330 |
|
| 1331 |
public function get_post_meta_context($post_id) { |
| 1332 |
if (get_option('rag_embed_meta') != '1') { |
| 1333 |
return ""; |
| 1334 |
} |
| 1335 |
|
| 1336 |
$meta_keys_str = get_option('rag_embed_meta_keys', ''); |
| 1337 |
$meta_context = ""; |
| 1338 |
|
| 1339 |
if (!empty($meta_keys_str)) { |
| 1340 |
$keys = array_map('trim', explode(',', $meta_keys_str)); |
| 1341 |
foreach ($keys as $key) { |
| 1342 |
$value = get_post_meta($post_id, $key, true); |
| 1343 |
if (!empty($value)) { |
| 1344 |
if (is_array($value) || is_object($value)) { |
| 1345 |
$value = json_encode($value); |
| 1346 |
} |
| 1347 |
$meta_context .= "\n" . ucfirst(str_replace('_', ' ', $key)) . ": " . $value; |
| 1348 |
} |
| 1349 |
} |
| 1350 |
} else { |
| 1351 |
$all_meta = get_post_meta($post_id); |
| 1352 |
foreach ($all_meta as $key => $values) { |
| 1353 |
if (strpos($key, '_') === 0) continue; |
| 1354 |
$value = $values[0]; |
| 1355 |
if (!empty($value)) { |
| 1356 |
$meta_context .= "\n" . ucfirst(str_replace('_', ' ', $key)) . ": " . $value; |
| 1357 |
} |
| 1358 |
} |
| 1359 |
} |
| 1360 |
|
| 1361 |
return $meta_context; |
| 1362 |
} |
| 1363 |
|
| 1364 |
} |
| 1365 |
} |
| 1366 |
if ( ! function_exists( 'Qcld_Bot_Rag' ) ) { |
| 1367 |
|
| 1368 |
function Qcld_Bot_Rag() { |
| 1369 |
return Qcld_Bot_Rag::instance(); |
| 1370 |
} |
| 1371 |
} |
| 1372 |
|
| 1373 |
// fire off the plugin. |
| 1374 |
Qcld_Bot_Rag(); |
| 1375 |
|