PluginProbe
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services / 8.4.1
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services v8.4.1
8.7.8 8.7.7 8.7.6 8.7.5 8.7.4 8.7.3 8.7.2 8.7.1 8.7.0 8.6.9 8.6.8 8.6.7 8.6.6 8.6.5 8.6.4 8.6.2 8.6.1 8.6.0 8.5.9 8.5.8 8.5.7 8.5.6 8.5.5 8.5.4 8.5.3 All 534 releases
chatbot / includes / class-qcld-bot-rag.php

class-qcld-bot-rag.php in WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services 8.4.1, at includes/class-qcld-bot-rag.php

1,362 lines 46.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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: {$filename}</p>";
285 continue;
286 }
287
288 $file_url = $upload['url'];
289 $file_path = $upload['file'];
290
291 echo "<p>Uploaded: $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: " . 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 $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($table, [
335 'title' => sanitize_text_field($title),
336 'content' => $content,
337 'embedding' => wp_json_encode($embedding),
338 'source_type' => 'csv',
339 'source_url' => $file_url,
340 'file_url' => $file_url,
341 'status' => 'complete',
342 'metadata' => wp_json_encode(['filename' => $filename, 'row' => $row_count]),
343 'created_at' => current_time('mysql')
344 ]);
345
346 if ($result !== false) {
347 $success_count++;
348 } else {
349 echo "<p style='color:red;'>DB error row $row_count: " . $wpdb->last_error . "</p>";
350 }
351 }
352
353 fclose($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions
354 echo "<p style='color:green;'>✓ Processed $success_count of $row_count rows</p>";
355 }
356
357 echo "<h3>CSV Processing Complete!</h3>";
358 }
359 public function wp_rag_process_pdf_upload() {
360 if (empty($_FILES['rag_pdf']['name'][0])) {
361 echo "<p>No PDF selected.</p>";
362 return;
363 }
364
365 require_once(ABSPATH . 'wp-admin/includes/file.php');
366 $uploaded_files = $_FILES['rag_pdf'];
367
368 foreach ($uploaded_files['name'] as $index => $filename) {
369
370 // Upload to WP Media
371 $file_array = [
372 'name' => $uploaded_files['name'][$index],
373 'type' => $uploaded_files['type'][$index],
374 'tmp_name' => $uploaded_files['tmp_name'][$index],
375 'error' => $uploaded_files['error'][$index],
376 'size' => $uploaded_files['size'][$index],
377 ];
378
379 $upload = wp_handle_upload($file_array, ['test_form' => false]);
380
381 if (isset($upload['error'])) {
382 echo "<p>Error uploading: {$filename}</p>";
383 continue;
384 }
385
386 $file_url = $upload['url'];
387 $file_path = $upload['file'];
388
389 echo "<p>Uploaded: $filename</p>";
390
391 // Extract PDF text (uses Smalot/PdfParser)
392 if (!class_exists('\Smalot\PdfParser\Parser')) {
393 echo "<p>PDF parser missing! Install `smalot/pdfparser`.</p>";
394 return;
395 }
396
397 $parser = new \Smalot\PdfParser\Parser();
398 $pdf = $parser->parseFile($file_path);
399 $text = $pdf->getText();
400
401 echo "<p>Extracted text length: ".strlen($text)."</p>";
402
403 // Generate Embedding
404 $embedding = $this->generate_embedding($text);
405
406 if (empty($embedding)) {
407 echo "<p style='color:red;'>Failed to generate embedding for: $filename</p>";
408 continue;
409 }
410
411 // Save to DB
412 global $wpdb;
413 $table = $wpdb->prefix . "rag_documents";
414
415 $result = $wpdb->insert($table, [
416 'title' => sanitize_text_field($filename),
417 'content' => $text,
418 'embedding' => wp_json_encode($embedding),
419 'source_type' => 'pdf',
420 'source_url' => $file_url,
421 'file_url' => $file_url,
422 'status' => 'complete',
423 'metadata' => wp_json_encode(['size' => $uploaded_files['size'][$index]]),
424 'created_at' => current_time('mysql')
425 ]);
426
427 if ($result === false) {
428 echo "<p style='color:red;'>Database error: " . $wpdb->last_error . "</p>";
429 } else {
430 echo "<p style='color:green;'>✓ Saved PDF embedding for: $filename (ID: " . $wpdb->insert_id . ")</p>";
431 }
432 }
433
434 echo "<h3>PDF Processing Complete!</h3>";
435 }
436
437 // XAML processing method
438 public function wp_rag_process_xaml_upload() {
439 if (empty($_FILES['rag_xaml']['name'][0])) {
440 echo "<p>No XAML selected.</p>";
441 return;
442 }
443
444 require_once(ABSPATH . 'wp-admin/includes/file.php');
445 $uploaded_files = $_FILES['rag_xaml'];
446
447 foreach ($uploaded_files['name'] as $index => $filename) {
448
449 $file_array = [
450 'name' => $uploaded_files['name'][$index],
451 'type' => $uploaded_files['type'][$index],
452 'tmp_name' => $uploaded_files['tmp_name'][$index],
453 'error' => $uploaded_files['error'][$index],
454 'size' => $uploaded_files['size'][$index],
455 ];
456
457 $upload = wp_handle_upload($file_array, [
458 'test_form' => false,
459 'test_type' => false, // Bypass mime type check
460 ]);
461
462 if (isset($upload['error'])) {
463 echo "<p>Error uploading {$filename}: " . $upload['error'] . "</p>";
464 continue;
465 }
466
467 $file_url = $upload['url'];
468 $file_path = $upload['file'];
469
470 echo "<p>Uploaded: $filename</p>";
471
472 // Read XAML/XML content
473 global $wp_filesystem;
474 if ( empty( $wp_filesystem ) ) {
475 require_once ABSPATH . '/wp-admin/includes/file.php';
476 WP_Filesystem();
477 }
478 $xml_content = $wp_filesystem->get_contents( $file_path );
479
480 if (empty($xml_content)) {
481 echo "<p style='color:red;'>Failed to read file or file is empty: $filename</p>";
482 continue;
483 }
484
485 // Attempt to parse as XML
486 $xml = simplexml_load_string($xml_content, 'SimpleXMLElement', LIBXML_NOCDATA);
487
488 $items_to_process = [];
489
490 if ($xml && isset($xml->channel->item)) {
491 // It's likely a WordPress export (WXR) file
492 echo "<p>Detected WordPress Export format. Extracting items...</p>";
493 foreach ($xml->channel->item as $item) {
494 $title = (string)$item->title;
495 $namespaces = $item->getNameSpaces(true);
496 $content = "";
497
498 if (isset($namespaces['content'])) {
499 $content = (string)$item->children($namespaces['content'])->encoded;
500 } else {
501 $content = (string)$item->description;
502 }
503
504 if (!empty($content)) {
505 $items_to_process[] = [
506 'title' => !empty($title) ? $title : $filename,
507 'content' => $content,
508 'source_url' => (string)$item->link
509 ];
510 }
511 }
512 } else {
513 // Treat as generic text/XML
514 $items_to_process[] = [
515 'title' => $filename,
516 'content' => $xml_content,
517 'source_url' => $file_url
518 ];
519 }
520
521 foreach ($items_to_process as $item_data) {
522 $clean_content = $this->clean_rag_content($item_data['content']);
523
524 if (empty($clean_content) || strlen($clean_content) < 20) {
525 continue;
526 }
527
528 // Generate Embedding
529 $embedding = $this->generate_embedding($clean_content);
530
531 if (empty($embedding)) {
532 echo "<p style='color:red;'>Failed to generate embedding for item: {$item_data['title']}</p>";
533 continue;
534 }
535
536 // Save to DB
537 global $wpdb;
538 $table = $wpdb->prefix . "rag_documents";
539
540 $result = $wpdb->insert($table, [
541 'title' => sanitize_text_field($item_data['title']),
542 'content' => $clean_content,
543 'embedding' => wp_json_encode($embedding),
544 'source_type' => 'xaml',
545 'source_url' => $item_data['source_url'] ? $item_data['source_url'] : $file_url,
546 'file_url' => $file_url,
547 'status' => 'complete',
548 'metadata' => wp_json_encode(['size' => strlen($clean_content)]),
549 'created_at' => current_time('mysql')
550 ]);
551
552 if ($result === false) {
553 echo "<p style='color:red;'>Database error for item {$item_data['title']}: " . $wpdb->last_error . "</p>";
554 } else {
555 echo "<p style='color:green;'>✓ Saved embedding for: {$item_data['title']} (ID: " . $wpdb->insert_id . ")</p>";
556 }
557 }
558 }
559
560 echo "<h3>XAML Processing Complete!</h3>";
561 }
562
563 public function wp_rag_embed_all_documents()
564 {
565 $apiKey = get_option('open_ai_api_key'); // Replace with option if needed
566 global $wpdb;
567
568 $posts = get_posts([
569 'post_type' => ['post', 'page'],
570 'posts_per_page' => -1
571 ]);
572
573 echo "<ul>";
574
575 foreach ($posts as $p) {
576 $content = wp_strip_all_tags($p->post_content);
577 if (strlen($content) < 20) continue;
578
579 $embedding = $this->wp_rag_create_embedding($content, $apiKey);
580
581 $wpdb->insert(
582 $wpdb->prefix . "rag_documents",
583 [
584 "title" => $p->post_title,
585 "content" => $content,
586 "embedding" => wp_json_encode($embedding)
587 ]
588 );
589
590 echo "<li>Embedded: " . esc_html($p->post_title) . "</li>";
591 flush();
592 }
593
594 echo "</ul>";
595 echo "<strong>Completed!</strong>";
596 }
597
598 public function wp_rag_embed_all_sources()
599 {
600 // $apiKey = get_option('open_ai_api_key');
601 global $wpdb;
602
603 $post_types = [];
604 if (get_option('rag_embed_pages') == '1') {
605 $post_types[] = 'page';
606 }
607 if (get_option('rag_embed_posts') == '1') {
608 $post_types[] = 'post';
609 }
610
611 $cpts = get_option('rag_embed_cpts', []);
612 if (!empty($cpts) && is_array($cpts)) {
613 $post_types = array_merge($post_types, $cpts);
614 }
615
616 $table = $wpdb->prefix . "rag_documents";
617 $updated_count = 0;
618 $inserted_count = 0;
619 $skipped_count = 0;
620
621 echo "<ul>";
622
623 if (!empty($post_types)) {
624 $posts = get_posts([
625 'post_type' => $post_types,
626 'posts_per_page' => -1,
627 'post_status' => 'publish'
628 ]);
629
630 foreach ($posts as $p) {
631 $title = $p->post_title;
632 $content = "Title: " . $title . "\n";
633 $content .= "Date: " . $p->post_date . "\n";
634
635 $main_content = strip_shortcodes($p->post_content);
636 $main_content = wp_strip_all_tags($main_content);
637 $content .= $main_content;
638
639 // Specific handling for WooCommerce Products
640 if ($p->post_type === 'product' && class_exists('WC_Product') && function_exists('wc_get_product')) {
641 $_product = wc_get_product($p->ID);
642 if ($_product) {
643 $price = $_product->get_price();
644 $currency = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol() : '$';
645 $content .= "\nPrice: " . $currency . $price;
646
647 // Add description if main content is empty
648 if (empty(trim($main_content)) && method_exists($_product, 'get_short_description')) {
649 $content .= "\nDescription: " . wp_strip_all_tags($_product->get_short_description());
650 }
651 }
652 }
653
654 if (strlen(trim($content)) < 20) {
655 $skipped_count++;
656 continue;
657 }
658
659 $embedding = $this->generate_embedding($content);
660
661 if (empty($embedding)) {
662 echo "<li style='color:red;'>Failed to generate embedding for: " . esc_html($p->post_title) . "</li>";
663 continue;
664 }
665
666 $table = $wpdb->prefix . "rag_documents";
667
668 // Check if this post already exists in the database
669 $existing = $wpdb->get_row($wpdb->prepare(
670 "SELECT id FROM $table WHERE metadata LIKE %s AND source_type = %s",
671 '%"post_id":' . $p->ID . '%',
672 $p->post_type
673 ));
674
675 $data = [
676 "title" => $p->post_title,
677 "content" => $content,
678 "embedding" => wp_json_encode($embedding),
679 "source_type" => $p->post_type,
680 "source_url" => get_permalink($p->ID),
681 "file_url" => get_permalink($p->ID),
682 "status" => 'complete',
683 "metadata" => wp_json_encode(['post_id' => $p->ID]),
684 "created_at" => current_time('mysql')
685 ];
686
687 if ($existing) {
688 // Update existing record
689 $wpdb->update(
690 $table,
691 $data,
692 ['id' => $existing->id]
693 );
694 echo "<li style='color:blue;'>✓ Updated: " . esc_html($p->post_title) . " (" . esc_html($p->post_type) . ")</li>";
695 $updated_count++;
696 } else {
697 // Insert new record
698 $wpdb->insert($table, $data);
699 echo "<li style='color:green;'>✓ Embedded: " . esc_html($p->post_title) . " (" . esc_html($p->post_type) . ")</li>";
700 $inserted_count++;
701 }
702
703 if (function_exists('flush')) {
704 @flush();
705 }
706 if (function_exists('ob_flush')) {
707 @ob_flush();
708 }
709 }
710 }
711
712 // Simple Text Responses Embedding
713 if (get_option('rag_embed_str') == '1') {
714 $str_table = $wpdb->prefix . 'wpbot_response';
715 $str_results = $wpdb->get_results("SELECT * FROM $str_table");
716
717 if (!empty($str_results)) {
718 foreach ($str_results as $str) {
719 $content = "Query: " . $str->query . "\n";
720 $content .= "Response: " . wp_strip_all_tags($str->response) . "\n";
721 if (!empty($str->keyword)) {
722 $content .= "Keywords: " . $str->keyword;
723 }
724
725 if (strlen(trim($content)) < 10) {
726 $skipped_count++;
727 continue;
728 }
729
730 $embedding = $this->generate_embedding($content);
731 if (empty($embedding)) {
732 echo "<li style='color:red;'>Failed to generate embedding for STR: " . esc_html($str->query) . "</li>";
733 continue;
734 }
735
736 // Check if this STR already exists in the RAG database
737 $existing = $wpdb->get_row($wpdb->prepare(
738 "SELECT id FROM $table WHERE metadata LIKE %s AND source_type = %s",
739 '%"str_id":' . $str->id . '%',
740 'str'
741 ));
742
743 $data = [
744 "title" => $str->query,
745 "content" => $content,
746 "embedding" => wp_json_encode($embedding),
747 "source_type" => 'str',
748 "source_url" => admin_url('admin.php?page=simple-text-response&action=edit&query=' . $str->id),
749 "file_url" => '',
750 "status" => 'complete',
751 "metadata" => wp_json_encode(['str_id' => $str->id]),
752 "created_at" => current_time('mysql')
753 ];
754
755 if ($existing) {
756 $wpdb->update($table, $data, ['id' => $existing->id]);
757 echo "<li style='color:blue;'>✓ Updated STR: " . esc_html($str->query) . "</li>";
758 $updated_count++;
759 } else {
760 $wpdb->insert($table, $data);
761 echo "<li style='color:green;'>✓ Embedded STR: " . esc_html($str->query) . "</li>";
762 $inserted_count++;
763 }
764
765 if (function_exists('flush')) { @flush(); }
766 if (function_exists('ob_flush')) { @ob_flush(); }
767 }
768 }
769 }
770
771 echo "</ul>";
772 echo "<h3>All Selected Sources Processed!</h3>";
773 echo "<p><strong>Summary:</strong></p>";
774 echo "<ul>";
775 echo "<li>New entries created: <strong>$inserted_count</strong></li>";
776 echo "<li>Existing entries updated: <strong style='color:blue;'>$updated_count</strong></li>";
777 echo "<li>Skipped (too short): <strong>$skipped_count</strong></li>";
778 echo "</ul>";
779 }
780 public function wp_rag_create_embedding($text, $apiKey)
781 {
782
783 $response = $this->generate_embedding($text);
784 return $response;
785 }
786 public function ajax_rag_manual_sync() {
787 check_ajax_referer('wp_chatbot', 'nonce');
788
789 $doc_id = isset($_POST['id']) ? intval($_POST['id']) : 0;
790 if (!$doc_id) {
791 wp_send_json_error(['message' => 'Invalid document ID']);
792 }
793
794 global $wpdb;
795 $table = $wpdb->prefix . 'rag_documents';
796 $doc = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $doc_id));
797
798 if (!$doc) {
799 wp_send_json_error(['message' => 'Document not found']);
800 }
801
802 // Try to extract post/product info
803 $post_id = 0;
804 if (!empty($doc->metadata)) {
805 $metadata = json_decode($doc->metadata, true);
806 if (isset($metadata['post_id'])) {
807 $post_id = intval($metadata['post_id']);
808 }
809 }
810
811 if (!$post_id && ($doc->source_type === 'page' || $doc->source_type === 'post' || $doc->source_type === 'xaml')) {
812 $post_id = url_to_postid($doc->source_url);
813 }
814
815 if (!$post_id) {
816 wp_send_json_error(['message' => 'Could not determine source post for manual sync']);
817 }
818
819 $result = $this->wp_rag_sync_post($post_id, true);
820
821 if (is_wp_error($result)) {
822 wp_send_json_error(['message' => $result->get_error_message()]);
823 }
824
825 wp_send_json_success(['message' => 'Document synced successfully!']);
826 }
827 public function qcld_rag_delete_document_callback() {
828 check_ajax_referer('wp_chatbot', 'nonce');
829 if (!current_user_can('manage_options')) {
830 wp_send_json_error('Unauthorized');
831 }
832
833 global $wpdb;
834 $id = intval($_POST['id']);
835 $table_name = $wpdb->prefix . 'rag_documents';
836
837 $deleted = $wpdb->delete($table_name, array('id' => $id));
838
839 if ($deleted) {
840 wp_send_json_success('Document deleted successfully.');
841 } else {
842 wp_send_json_error('Failed to delete document.');
843 }
844 }
845 public function qcld_rag_bulk_delete_documents_callback() {
846 check_ajax_referer('wp_chatbot', 'nonce');
847 if (!current_user_can('manage_options')) {
848 wp_send_json_error('Unauthorized');
849 }
850
851 if (empty($_POST['ids']) || !is_array($_POST['ids'])) {
852 wp_send_json_error('No documents selected.');
853 }
854
855 global $wpdb;
856 $ids = array_map('intval', $_POST['ids']);
857 $table_name = $wpdb->prefix . 'rag_documents';
858
859 $ids_string = implode(',', $ids);
860 $deleted = $wpdb->query("DELETE FROM $table_name WHERE id IN ($ids_string)");
861
862 if ($deleted !== false) {
863 wp_send_json_success('Selected documents deleted successfully.');
864 } else {
865 wp_send_json_error('Failed to delete selected documents.');
866 }
867 }
868 public function qcld_rag_delete_all_documents_callback() {
869 check_ajax_referer('wp_chatbot', 'nonce');
870 if (!current_user_can('manage_options')) {
871 wp_send_json_error('Unauthorized');
872 }
873
874 global $wpdb;
875 $table_name = $wpdb->prefix . 'rag_documents';
876
877 $deleted = $wpdb->query("TRUNCATE TABLE $table_name");
878
879 // Some DBs might not support TRUNCATE on tables with foreign keys or other constraints,
880 // though rag_documents is likely simple. Fallback to DELETE.
881 if ($deleted === false) {
882 $deleted = $wpdb->query("DELETE FROM $table_name");
883 }
884
885 if ($deleted !== false) {
886 wp_send_json_success('All documents deleted successfully.');
887 } else {
888 wp_send_json_error('Failed to delete all documents.');
889 }
890 }
891 public function qcld_rag_get_document_callback() {
892 check_ajax_referer('wp_chatbot', 'nonce');
893 if (!current_user_can('manage_options')) {
894 wp_send_json_error('Unauthorized');
895 }
896
897 global $wpdb;
898 $id = intval($_POST['id']);
899 $table_name = $wpdb->prefix . 'rag_documents';
900
901 $document = $wpdb->get_row($wpdb->prepare("SELECT id, title, content FROM $table_name WHERE id = %d", $id));
902
903 if ($document) {
904 wp_send_json_success($document);
905 } else {
906 wp_send_json_error('Document not found.');
907 }
908 }
909 public function qcld_rag_update_document_callback() {
910 check_ajax_referer('wp_chatbot', 'nonce');
911 if (!current_user_can('manage_options')) {
912 wp_send_json_error('Unauthorized');
913 }
914
915 global $wpdb;
916 $id = intval(wp_unslash($_POST['id']));
917 $title = sanitize_text_field(wp_unslash($_POST['title']));
918 $content = sanitize_textarea_field(wp_unslash($_POST['content']));
919 $table_name = $wpdb->prefix . 'rag_documents';
920
921 // Re-generate embedding if content changed
922 $old_content = $wpdb->get_var($wpdb->prepare("SELECT content FROM $table_name WHERE id = %d", $id));
923
924 $update_data = array(
925 'title' => $title,
926 'content' => $content,
927 'status' => 'complete'
928 );
929
930 if ($old_content !== $content) {
931 $embedding = $this->generate_embedding($content);
932 if (!empty($embedding)) {
933 $update_data['embedding'] = wp_json_encode($embedding);
934 } else {
935 $update_data['status'] = 'error';
936 }
937 }
938
939 $updated = $wpdb->update($table_name, $update_data, array('id' => $id));
940
941 if ($updated !== false) {
942 wp_send_json_success('Document updated successfully.');
943 } else {
944 wp_send_json_error('Failed to update document.');
945 }
946 }
947 public function wp_rag_sync_post($post_id, $force = false) {
948 $post = get_post($post_id);
949 if (!$post) return new WP_Error('invalid_post', 'Post not found');
950
951 $title = $post->post_title;
952 $url = get_permalink($post_id);
953 $content = "Title: " . $title . "\n";
954 $content .= "Date: " . $post->post_date . "\n";
955
956 $main_content = strip_shortcodes($post->post_content);
957 $main_content = wp_strip_all_tags($main_content);
958 $content .= $main_content;
959
960 // Specific handling for WooCommerce Products
961 if ($post->post_type === 'product' && class_exists('WC_Product') && function_exists('wc_get_product')) {
962 $_product = wc_get_product($post_id);
963 if ($_product) {
964 $price = $_product->get_price();
965 $currency = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol() : '$';
966 $content .= "\nPrice: " . $currency . $price;
967
968 // Add description if main content is empty (sometimes WC uses short description)
969 if (empty($main_content) && method_exists($_product, 'get_short_description')) {
970 $content .= "\nDescription: " . wp_strip_all_tags($_product->get_short_description());
971 }
972 }
973 }
974
975 if (empty(trim($main_content)) && !($post->post_type === 'product')) {
976 return new WP_Error('empty_content', 'No content found to embed');
977 }
978
979 // Generate Embedding
980 $embedding = $this->generate_embedding($content);
981 if (empty($embedding)) {
982 return new WP_Error('embedding_failed', 'Failed to generate embedding');
983 }
984
985 global $wpdb;
986 $table = $wpdb->prefix . "rag_documents";
987
988 // Check if it already exists (by source_url or custom metadata if we had it)
989 $existing = $wpdb->get_row($wpdb->prepare("SELECT id FROM $table WHERE source_url = %s", $url));
990
991 if ($existing) {
992 $result = $wpdb->update($table, [
993 'title' => sanitize_text_field($title),
994 'content' => $content,
995 'embedding' => wp_json_encode($embedding),
996 'status' => 'complete',
997 'created_at' => current_time('mysql')
998 ], ['id' => $existing->id]);
999 } else {
1000 $result = $wpdb->insert($table, [
1001 'title' => sanitize_text_field($title),
1002 'content' => $content,
1003 'embedding' => wp_json_encode($embedding),
1004 'source_type' => ($post->post_type === 'page' || $post->post_type === 'post') ? $post->post_type : 'xaml',
1005 'source_url' => $url,
1006 'file_url' => $url,
1007 'status' => 'complete',
1008 'metadata' => wp_json_encode(['post_id' => $post_id, 'post_type' => $post->post_type]),
1009 'created_at' => current_time('mysql')
1010 ]);
1011 }
1012
1013 return $result;
1014 }
1015 public function wp_rag_handle_auto_sync_hook($post_id, $post, $update) {
1016 // Only run if auto-sync is enabled
1017 if (get_option('rag_auto_sync_enabled') != '1') {
1018 return;
1019 }
1020
1021 // Avoid autosaves and revisions
1022 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
1023 if (wp_is_post_revision($post_id)) return;
1024 if ($post->post_status != 'publish') return;
1025
1026 // Check if post type is enabled in general embedding settings
1027 $is_allowed = false;
1028 if ($post->post_type === 'page' && get_option('rag_embed_pages') == '1') {
1029 $is_allowed = true;
1030 } elseif ($post->post_type === 'post' && get_option('rag_embed_posts') == '1') {
1031 $is_allowed = true;
1032 } else {
1033 $cpts = get_option('rag_embed_cpts', []);
1034 if (is_array($cpts) && in_array($post->post_type, $cpts)) {
1035 $is_allowed = true;
1036 }
1037 }
1038
1039 if (!$is_allowed) {
1040 return;
1041 }
1042
1043 $this->wp_rag_sync_post($post_id);
1044 }
1045
1046 public function clean_rag_content($text) {
1047 if (empty($text)) return "";
1048
1049 // Remove WordPress block comments like <!-- wp:paragraph -->
1050 $text = preg_replace('/<!--\s*\/?[a-z0-9_-]+:[a-z0-9_-]+\s*({.*?})?\s*-->/s', '', $text);
1051
1052 // Remove generic HTML comments
1053 $text = preg_replace('/<!--(.*?)-->/s', '', $text);
1054
1055 // Strip HTML tags
1056 $text = wp_strip_all_tags($text);
1057
1058 // Decode HTML entities
1059 $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
1060
1061 // Normalize whitespace
1062 $text = preg_replace('/\s+/', ' ', $text);
1063
1064 return trim($text);
1065 }
1066
1067 public function run_rag_search($user_query, $top_k = 3) {
1068 global $wpdb;
1069 $table = $wpdb->prefix . "rag_documents";
1070
1071 // Get all embeddings and texts
1072 $rows = $wpdb->get_results("SELECT content, embedding FROM $table WHERE status = 'complete'", ARRAY_A);
1073
1074 if (empty($rows)) {
1075 return "No knowledge base found.";
1076 }
1077
1078 // Step 1: Get embedding for user query
1079 $query_vector = $this->generate_embedding($user_query);
1080
1081 if (empty($query_vector)) {
1082 return "";
1083 }
1084
1085 // Step 2: Compute cosine similarity
1086 $ranked = [];
1087 foreach ($rows as $row) {
1088 $doc_embedding = json_decode($row['embedding'], true);
1089 if (!is_array($doc_embedding) || empty($doc_embedding)) {
1090 continue;
1091 }
1092 $sim = $this->cosine_similarity($query_vector, $doc_embedding);
1093 $ranked[] = ["score" => $sim, "text" => $row['content']];
1094 }
1095
1096 // Step 3: Sort by similarity
1097 usort($ranked, function ($a, $b) {
1098 return $a['score'] < $b['score'] ? 1 : -1;
1099 });
1100
1101 // Select top k documents
1102 $top_docs = array_slice($ranked, 0, $top_k);
1103
1104 $context = "";
1105 foreach ($top_docs as $doc) {
1106 $context .= $doc["text"] . "\n\n";
1107 }
1108
1109 return trim($context);
1110 }
1111
1112 private function cosine_similarity($vecA, $vecB) {
1113 if (!is_array($vecA) || !is_array($vecB) || count($vecA) !== count($vecB)) {
1114 return 0.0;
1115 }
1116 $dot = 0.0;
1117 $normA = 0.0;
1118 $normB = 0.0;
1119
1120 $len = count($vecA);
1121 for ($i = 0; $i < $len; $i++) {
1122 $dot += $vecA[$i] * $vecB[$i];
1123 $normA += $vecA[$i] ** 2;
1124 $normB += $vecB[$i] ** 2;
1125 }
1126
1127 if ($normA == 0 || $normB == 0) {
1128 return 0.0;
1129 }
1130
1131 return $dot / (sqrt($normA) * sqrt($normB));
1132 }
1133
1134 public function ajax_qcld_rag_get_embed_queue() {
1135 check_ajax_referer('wp_chatbot', 'nonce');
1136 if (!current_user_can('manage_options')) {
1137 wp_send_json_error('Unauthorized');
1138 }
1139
1140 global $wpdb;
1141 $queue = [];
1142
1143 // Posts, Pages, CPTs
1144 $post_types = [];
1145 if (get_option('rag_embed_pages') == '1') {
1146 $post_types[] = 'page';
1147 }
1148 if (get_option('rag_embed_posts') == '1') {
1149 $post_types[] = 'post';
1150 }
1151
1152 $cpts = get_option('rag_embed_cpts', []);
1153 if (!empty($cpts) && is_array($cpts)) {
1154 $post_types = array_merge($post_types, $cpts);
1155 }
1156
1157 if (!empty($post_types)) {
1158 $posts = get_posts([
1159 'post_type' => $post_types,
1160 'posts_per_page' => -1,
1161 'post_status' => 'publish',
1162 'fields' => 'ids'
1163 ]);
1164 foreach ($posts as $post_id) {
1165 $queue[] = ['id' => $post_id, 'type' => 'post'];
1166 }
1167 }
1168
1169 // Simple Text Responses
1170 if (get_option('rag_embed_str') == '1') {
1171 $str_ids = $wpdb->get_col("SELECT id FROM {$wpdb->prefix}wpbot_response");
1172 foreach ($str_ids as $str_id) {
1173 $queue[] = ['id' => $str_id, 'type' => 'str'];
1174 }
1175 }
1176
1177 wp_send_json_success($queue);
1178 }
1179
1180 public function ajax_qcld_rag_process_item() {
1181 check_ajax_referer('wp_chatbot', 'nonce');
1182 if (!current_user_can('manage_options')) {
1183 wp_send_json_error('Unauthorized');
1184 }
1185
1186 $id = intval($_POST['item_id']);
1187 $type = sanitize_text_field($_POST['item_type']);
1188
1189 if ($type === 'post') {
1190 $p = get_post($id);
1191 if (!$p) {
1192 wp_send_json_error('Post not found');
1193 }
1194
1195 global $wpdb;
1196 $table = $wpdb->prefix . "rag_documents";
1197 $apiKey = get_option('open_ai_api_key');
1198
1199 $title = $p->post_title;
1200 $content = "Title: " . $title . "\n";
1201
1202
1203 $main_content = strip_shortcodes($p->post_content);
1204 $main_content = wp_strip_all_tags($main_content);
1205 $content .= $main_content;
1206
1207 // Add Post Meta Context
1208 $content .= $this->get_post_meta_context($id);
1209
1210 if ($p->post_type === 'product' && class_exists('WC_Product')) {
1211 $_product = wc_get_product($p->ID);
1212 if ($_product) {
1213 $price = $_product->get_price();
1214 $currency = get_woocommerce_currency_symbol();
1215 $content .= "\nPrice: " . $currency . $price;
1216 if (empty(trim($main_content))) {
1217 $content .= "\nDescription: " . wp_strip_all_tags($_product->get_short_description());
1218 }
1219 $content .= "\nProduct Link: " . get_permalink($p->ID);
1220 $content .= "\nProduct ID: " . $p->ID;
1221
1222 }
1223 }
1224
1225 if (strlen(trim($content)) < 20) {
1226 wp_send_json_success(['status' => 'skipped', 'title' => $title]);
1227 }
1228
1229 $embedding = $this->wp_rag_create_embedding($content, $apiKey);
1230 if (empty($embedding) || is_wp_error($embedding)) {
1231 $error_msg = is_wp_error($embedding) ? $embedding->get_error_message() : 'Failed to generate embedding';
1232 wp_send_json_error($error_msg);
1233 }
1234
1235 $existing = $wpdb->get_row($wpdb->prepare(
1236 "SELECT id FROM $table WHERE metadata LIKE %s AND source_type = %s",
1237 '%"post_id":' . $p->ID . '%',
1238 $p->post_type
1239 ));
1240
1241 $data = [
1242 "title" => $p->post_title,
1243 "content" => $content,
1244 "embedding" => wp_json_encode($embedding),
1245 "source_type" => $p->post_type,
1246 "source_url" => get_permalink($p->ID),
1247 "file_url" => get_permalink($p->ID),
1248 "status" => 'complete',
1249 "metadata" => wp_json_encode(['post_id' => $p->ID]),
1250 "created_at" => current_time('mysql')
1251 ];
1252
1253 if ($existing) {
1254 $wpdb->update($table, $data, ['id' => $existing->id]);
1255 wp_send_json_success(['status' => 'updated', 'title' => $title]);
1256 } else {
1257 $wpdb->insert($table, $data);
1258 wp_send_json_success(['status' => 'inserted', 'title' => $title]);
1259 }
1260
1261 } elseif ($type === 'str') {
1262 global $wpdb;
1263 $str = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}wpbot_response WHERE id = %d", $id));
1264 if (!$str) {
1265 wp_send_json_error('STR not found');
1266 }
1267
1268 $content = "Query: " . $str->query . "\n";
1269 $content .= "Response: " . wp_strip_all_tags($str->response) . "\n";
1270 if (!empty($str->keyword)) {
1271 $content .= "Keywords: " . $str->keyword . "\n";
1272 }
1273 if (!empty($str->intent)) {
1274 $content .= "Intent: " . $str->intent . "\n";
1275 }
1276
1277 if (strlen(trim($content)) < 20) {
1278 wp_send_json_error('No content found to embed');
1279 }
1280
1281 $embedding = $this->generate_embedding($content);
1282
1283 if (empty($embedding) || is_wp_error($embedding)) {
1284 wp_send_json_error('Failed to generate embedding');
1285 }
1286
1287 $table = $wpdb->prefix . "rag_documents";
1288
1289 $existing = $wpdb->get_row($wpdb->prepare(
1290 "SELECT id FROM $table WHERE metadata LIKE %s AND source_type = %s",
1291 '%"str_id":' . $str->id . '%',
1292 'str'
1293 ));
1294
1295 $data = [
1296 "title" => $str->query,
1297 "content" => $content,
1298 "embedding" => wp_json_encode($embedding),
1299 "source_type" => 'str',
1300 "source_url" => '',
1301 "file_url" => '',
1302 "status" => 'complete',
1303 "metadata" => wp_json_encode(['str_id' => $str->id]),
1304 "created_at" => current_time('mysql')
1305 ];
1306
1307 if ($existing) {
1308 $wpdb->update($table, $data, ['id' => $existing->id]);
1309 } else {
1310 $wpdb->insert($table, $data);
1311 }
1312 wp_send_json_success(['status' => 'processed', 'title' => 'Simple Text Response ID ' . $id]);
1313 }
1314
1315 wp_send_json_error('Invalid item type');
1316 }
1317
1318 public function get_post_meta_context($post_id) {
1319 if (get_option('rag_embed_meta') != '1') {
1320 return "";
1321 }
1322
1323 $meta_keys_str = get_option('rag_embed_meta_keys', '');
1324 $meta_context = "";
1325
1326 if (!empty($meta_keys_str)) {
1327 $keys = array_map('trim', explode(',', $meta_keys_str));
1328 foreach ($keys as $key) {
1329 $value = get_post_meta($post_id, $key, true);
1330 if (!empty($value)) {
1331 if (is_array($value) || is_object($value)) {
1332 $value = json_encode($value);
1333 }
1334 $meta_context .= "\n" . ucfirst(str_replace('_', ' ', $key)) . ": " . $value;
1335 }
1336 }
1337 } else {
1338 $all_meta = get_post_meta($post_id);
1339 foreach ($all_meta as $key => $values) {
1340 if (strpos($key, '_') === 0) continue;
1341 $value = $values[0];
1342 if (!empty($value)) {
1343 $meta_context .= "\n" . ucfirst(str_replace('_', ' ', $key)) . ": " . $value;
1344 }
1345 }
1346 }
1347
1348 return $meta_context;
1349 }
1350
1351 }
1352 }
1353 if ( ! function_exists( 'Qcld_Bot_Rag' ) ) {
1354
1355 function Qcld_Bot_Rag() {
1356 return Qcld_Bot_Rag::instance();
1357 }
1358 }
1359
1360 // fire off the plugin.
1361 Qcld_Bot_Rag();
1362