PluginProbe
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services / 8.7.1
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services v8.7.1
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 8.5.2 8.5.0 All 532 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.7.1, at includes/class-qcld-bot-rag.php

1,375 lines 50.7 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: " . 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