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

1,371 lines 50.6 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 check_ajax_referer('wp_chatbot', 'nonce');
793
794 $doc_id = isset($_POST['id']) ? intval($_POST['id']) : 0;
795 if (!$doc_id) {
796 wp_send_json_error(['message' => 'Invalid document ID']);
797 }
798
799 global $wpdb;
800 $table = $wpdb->prefix . 'rag_documents';
801 $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
802
803 if (!$doc) {
804 wp_send_json_error(['message' => 'Document not found']);
805 }
806
807 // Try to extract post/product info
808 $post_id = 0;
809 if (!empty($doc->metadata)) {
810 $metadata = json_decode($doc->metadata, true);
811 if (isset($metadata['post_id'])) {
812 $post_id = intval($metadata['post_id']);
813 }
814 }
815
816 if (!$post_id && ($doc->source_type === 'page' || $doc->source_type === 'post' || $doc->source_type === 'xaml')) {
817 $post_id = url_to_postid($doc->source_url);
818 }
819
820 if (!$post_id) {
821 wp_send_json_error(['message' => 'Could not determine source post for manual sync']);
822 }
823
824 $result = $this->wp_rag_sync_post($post_id, true);
825
826 if (is_wp_error($result)) {
827 wp_send_json_error(['message' => $result->get_error_message()]);
828 }
829
830 wp_send_json_success(['message' => 'Document synced successfully!']);
831 }
832 public function qcld_rag_delete_document_callback() {
833 check_ajax_referer('wp_chatbot', 'nonce');
834 if (!current_user_can('manage_options')) {
835 wp_send_json_error('Unauthorized');
836 }
837
838 global $wpdb;
839 $id = intval($_POST['id']);
840 $table_name = $wpdb->prefix . 'rag_documents';
841
842 $deleted = $wpdb->delete($table_name, array('id' => $id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
843
844 if ($deleted) {
845 wp_send_json_success('Document deleted successfully.');
846 } else {
847 wp_send_json_error('Failed to delete document.');
848 }
849 }
850 public function qcld_rag_bulk_delete_documents_callback() {
851 check_ajax_referer('wp_chatbot', 'nonce');
852 if (!current_user_can('manage_options')) {
853 wp_send_json_error('Unauthorized');
854 }
855
856 if (empty($_POST['ids']) || !is_array($_POST['ids'])) {
857 wp_send_json_error('No documents selected.');
858 }
859
860 global $wpdb;
861 $ids = array_map('intval', $_POST['ids']);
862 $table_name = $wpdb->prefix . 'rag_documents';
863
864 $ids_string = implode(',', $ids);
865 $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
866
867 if ($deleted !== false) {
868 wp_send_json_success('Selected documents deleted successfully.');
869 } else {
870 wp_send_json_error('Failed to delete selected documents.');
871 }
872 }
873 public function qcld_rag_delete_all_documents_callback() {
874 check_ajax_referer('wp_chatbot', 'nonce');
875 if (!current_user_can('manage_options')) {
876 wp_send_json_error('Unauthorized');
877 }
878
879 global $wpdb;
880 $table_name = $wpdb->prefix . 'rag_documents';
881
882 $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
883
884 // Some DBs might not support TRUNCATE on tables with foreign keys or other constraints,
885 // though rag_documents is likely simple. Fallback to DELETE.
886 if ($deleted === false) {
887 $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
888 }
889
890 if ($deleted !== false) {
891 wp_send_json_success('All documents deleted successfully.');
892 } else {
893 wp_send_json_error('Failed to delete all documents.');
894 }
895 }
896 public function qcld_rag_get_document_callback() {
897 check_ajax_referer('wp_chatbot', 'nonce');
898 if (!current_user_can('manage_options')) {
899 wp_send_json_error('Unauthorized');
900 }
901
902 global $wpdb;
903 $id = intval($_POST['id']);
904 $table_name = $wpdb->prefix . 'rag_documents';
905
906 $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
907
908 if ($document) {
909 wp_send_json_success($document);
910 } else {
911 wp_send_json_error('Document not found.');
912 }
913 }
914 public function qcld_rag_update_document_callback() {
915 check_ajax_referer('wp_chatbot', 'nonce');
916 if (!current_user_can('manage_options')) {
917 wp_send_json_error('Unauthorized');
918 }
919
920 global $wpdb;
921 $id = intval(wp_unslash($_POST['id']));
922 $title = sanitize_text_field(wp_unslash($_POST['title']));
923 $content = sanitize_textarea_field(wp_unslash($_POST['content']));
924 $table_name = $wpdb->prefix . 'rag_documents';
925
926 // Re-generate embedding if content changed
927 $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
928
929 $update_data = array(
930 'title' => $title,
931 'content' => $content,
932 'status' => 'complete'
933 );
934
935 if ($old_content !== $content) {
936 $embedding = $this->generate_embedding($content);
937 if (!empty($embedding)) {
938 $update_data['embedding'] = wp_json_encode($embedding);
939 } else {
940 $update_data['status'] = 'error';
941 }
942 }
943
944 $updated = $wpdb->update($table_name, $update_data, array('id' => $id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
945
946 if ($updated !== false) {
947 wp_send_json_success('Document updated successfully.');
948 } else {
949 wp_send_json_error('Failed to update document.');
950 }
951 }
952 public function wp_rag_sync_post($post_id, $force = false) {
953 $post = get_post($post_id);
954 if (!$post) return new WP_Error('invalid_post', 'Post not found');
955
956 $title = $post->post_title;
957 $url = get_permalink($post_id);
958 $content = "Title: " . $title . "\n";
959 $content .= "Date: " . $post->post_date . "\n";
960
961 $main_content = strip_shortcodes($post->post_content);
962 $main_content = wp_strip_all_tags($main_content);
963 $content .= $main_content;
964
965 // Specific handling for WooCommerce Products
966 if ($post->post_type === 'product' && class_exists('WC_Product') && function_exists('wc_get_product')) {
967 $_product = wc_get_product($post_id);
968 if ($_product) {
969 $price = $_product->get_price();
970 $currency = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol() : '$';
971 $content .= "\nPrice: " . $currency . $price;
972
973 // Add description if main content is empty (sometimes WC uses short description)
974 if (empty($main_content) && method_exists($_product, 'get_short_description')) {
975 $content .= "\nDescription: " . wp_strip_all_tags($_product->get_short_description());
976 }
977 }
978 }
979
980 if (empty(trim($main_content)) && !($post->post_type === 'product')) {
981 return new WP_Error('empty_content', 'No content found to embed');
982 }
983
984 // Generate Embedding
985 $embedding = $this->generate_embedding($content);
986 if (empty($embedding)) {
987 return new WP_Error('embedding_failed', 'Failed to generate embedding');
988 }
989
990 global $wpdb;
991 $table = $wpdb->prefix . "rag_documents";
992
993 // Check if it already exists (by source_url or custom metadata if we had it)
994 $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
995
996 if ($existing) {
997 $result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
998 $table, [
999 'title' => sanitize_text_field($title),
1000 'content' => $content,
1001 'embedding' => wp_json_encode($embedding),
1002 'status' => 'complete',
1003 'created_at' => current_time('mysql')
1004 ], ['id' => $existing->id]);
1005 } else {
1006 $result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1007 $table, [
1008 'title' => sanitize_text_field($title),
1009 'content' => $content,
1010 'embedding' => wp_json_encode($embedding),
1011 'source_type' => ($post->post_type === 'page' || $post->post_type === 'post') ? $post->post_type : 'xaml',
1012 'source_url' => $url,
1013 'file_url' => $url,
1014 'status' => 'complete',
1015 'metadata' => wp_json_encode(['post_id' => $post_id, 'post_type' => $post->post_type]),
1016 'created_at' => current_time('mysql')
1017 ]);
1018 }
1019
1020 return $result;
1021 }
1022 public function wp_rag_handle_auto_sync_hook($post_id, $post, $update) {
1023 // Only run if auto-sync is enabled
1024 if (get_option('rag_auto_sync_enabled') != '1') {
1025 return;
1026 }
1027
1028 // Avoid autosaves and revisions
1029 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
1030 if (wp_is_post_revision($post_id)) return;
1031 if ($post->post_status != 'publish') return;
1032
1033 // Check if post type is enabled in general embedding settings
1034 $is_allowed = false;
1035 if ($post->post_type === 'page' && get_option('rag_embed_pages') == '1') {
1036 $is_allowed = true;
1037 } elseif ($post->post_type === 'post' && get_option('rag_embed_posts') == '1') {
1038 $is_allowed = true;
1039 } else {
1040 $cpts = get_option('rag_embed_cpts', []);
1041 if (is_array($cpts) && in_array($post->post_type, $cpts)) {
1042 $is_allowed = true;
1043 }
1044 }
1045
1046 if (!$is_allowed) {
1047 return;
1048 }
1049
1050 $this->wp_rag_sync_post($post_id);
1051 }
1052
1053 public function clean_rag_content($text) {
1054 if (empty($text)) return "";
1055
1056 // Remove WordPress block comments like <!-- wp:paragraph -->
1057 $text = preg_replace('/<!--\s*\/?[a-z0-9_-]+:[a-z0-9_-]+\s*({.*?})?\s*-->/s', '', $text);
1058
1059 // Remove generic HTML comments
1060 $text = preg_replace('/<!--(.*?)-->/s', '', $text);
1061
1062 // Strip HTML tags
1063 $text = wp_strip_all_tags($text);
1064
1065 // Decode HTML entities
1066 $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
1067
1068 // Normalize whitespace
1069 $text = preg_replace('/\s+/', ' ', $text);
1070
1071 return trim($text);
1072 }
1073
1074 public function run_rag_search($user_query, $top_k = 3) {
1075 global $wpdb;
1076 $table = $wpdb->prefix . "rag_documents";
1077
1078 // Get all embeddings and texts
1079 $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
1080
1081 if (empty($rows)) {
1082 return "No knowledge base found.";
1083 }
1084
1085 // Step 1: Get embedding for user query
1086 $query_vector = $this->generate_embedding($user_query);
1087
1088 if (empty($query_vector)) {
1089 return "";
1090 }
1091
1092 // Step 2: Compute cosine similarity
1093 $ranked = [];
1094 foreach ($rows as $row) {
1095 $doc_embedding = json_decode($row['embedding'], true);
1096 if (!is_array($doc_embedding) || empty($doc_embedding)) {
1097 continue;
1098 }
1099 $sim = $this->cosine_similarity($query_vector, $doc_embedding);
1100 $ranked[] = ["score" => $sim, "text" => $row['content']];
1101 }
1102
1103 // Step 3: Sort by similarity
1104 usort($ranked, function ($a, $b) {
1105 return $a['score'] < $b['score'] ? 1 : -1;
1106 });
1107
1108 // Select top k documents
1109 $top_docs = array_slice($ranked, 0, $top_k);
1110
1111 $context = "";
1112 foreach ($top_docs as $doc) {
1113 $context .= $doc["text"] . "\n\n";
1114 }
1115
1116 return trim($context);
1117 }
1118
1119 private function cosine_similarity($vecA, $vecB) {
1120 if (!is_array($vecA) || !is_array($vecB) || count($vecA) !== count($vecB)) {
1121 return 0.0;
1122 }
1123 $dot = 0.0;
1124 $normA = 0.0;
1125 $normB = 0.0;
1126
1127 $len = count($vecA);
1128 for ($i = 0; $i < $len; $i++) {
1129 $dot += $vecA[$i] * $vecB[$i];
1130 $normA += $vecA[$i] ** 2;
1131 $normB += $vecB[$i] ** 2;
1132 }
1133
1134 if ($normA == 0 || $normB == 0) {
1135 return 0.0;
1136 }
1137
1138 return $dot / (sqrt($normA) * sqrt($normB));
1139 }
1140
1141 public function ajax_qcld_rag_get_embed_queue() {
1142 check_ajax_referer('wp_chatbot', 'nonce');
1143 if (!current_user_can('manage_options')) {
1144 wp_send_json_error('Unauthorized');
1145 }
1146
1147 global $wpdb;
1148 $queue = [];
1149
1150 // Posts, Pages, CPTs
1151 $post_types = [];
1152 if (get_option('rag_embed_pages') == '1') {
1153 $post_types[] = 'page';
1154 }
1155 if (get_option('rag_embed_posts') == '1') {
1156 $post_types[] = 'post';
1157 }
1158
1159 $cpts = get_option('rag_embed_cpts', []);
1160 if (!empty($cpts) && is_array($cpts)) {
1161 $post_types = array_merge($post_types, $cpts);
1162 }
1163
1164 if (!empty($post_types)) {
1165 $posts = get_posts([
1166 'post_type' => $post_types,
1167 'posts_per_page' => -1,
1168 'post_status' => 'publish',
1169 'fields' => 'ids'
1170 ]);
1171 foreach ($posts as $post_id) {
1172 $queue[] = ['id' => $post_id, 'type' => 'post'];
1173 }
1174 }
1175
1176 // Simple Text Responses
1177 if (get_option('rag_embed_str') == '1') {
1178 $str_ids = $wpdb->get_col("SELECT id FROM {$wpdb->prefix}wpbot_response"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1179 foreach ($str_ids as $str_id) {
1180 $queue[] = ['id' => $str_id, 'type' => 'str'];
1181 }
1182 }
1183
1184 wp_send_json_success($queue);
1185 }
1186
1187 public function ajax_qcld_rag_process_item() {
1188 check_ajax_referer('wp_chatbot', 'nonce');
1189 if (!current_user_can('manage_options')) {
1190 wp_send_json_error('Unauthorized');
1191 }
1192
1193 $id = intval($_POST['item_id']);
1194 $type = sanitize_text_field($_POST['item_type']);
1195
1196 if ($type === 'post') {
1197 $p = get_post($id);
1198 if (!$p) {
1199 wp_send_json_error('Post not found');
1200 }
1201
1202 global $wpdb;
1203 $table = $wpdb->prefix . "rag_documents";
1204 $apiKey = get_option('open_ai_api_key');
1205
1206 $title = $p->post_title;
1207 $content = "Title: " . $title . "\n";
1208
1209
1210 $main_content = strip_shortcodes($p->post_content);
1211 $main_content = wp_strip_all_tags($main_content);
1212 $content .= $main_content;
1213
1214 // Add Post Meta Context
1215 $content .= $this->get_post_meta_context($id);
1216
1217 if ($p->post_type === 'product' && class_exists('WC_Product')) {
1218 $_product = wc_get_product($p->ID);
1219 if ($_product) {
1220 $price = $_product->get_price();
1221 $currency = get_woocommerce_currency_symbol();
1222 $content .= "\nPrice: " . $currency . $price;
1223 if (empty(trim($main_content))) {
1224 $content .= "\nDescription: " . wp_strip_all_tags($_product->get_short_description());
1225 }
1226 $content .= "\nProduct Link: " . get_permalink($p->ID);
1227 $content .= "\nProduct ID: " . $p->ID;
1228
1229 }
1230 }
1231
1232 if (strlen(trim($content)) < 20) {
1233 wp_send_json_success(['status' => 'skipped', 'title' => $title]);
1234 }
1235
1236 $embedding = $this->wp_rag_create_embedding($content, $apiKey);
1237 if (empty($embedding) || is_wp_error($embedding)) {
1238 $error_msg = is_wp_error($embedding) ? $embedding->get_error_message() : 'Failed to generate embedding';
1239 wp_send_json_error($error_msg);
1240 }
1241
1242 $existing = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
1243 $wpdb->prepare(
1244 "SELECT id FROM $table WHERE metadata LIKE %s AND source_type = %s",
1245 '%"post_id":' . $p->ID . '%',
1246 $p->post_type
1247 ));
1248
1249 $data = [
1250 "title" => $p->post_title,
1251 "content" => $content,
1252 "embedding" => wp_json_encode($embedding),
1253 "source_type" => $p->post_type,
1254 "source_url" => get_permalink($p->ID),
1255 "file_url" => get_permalink($p->ID),
1256 "status" => 'complete',
1257 "metadata" => wp_json_encode(['post_id' => $p->ID]),
1258 "created_at" => current_time('mysql')
1259 ];
1260
1261 if ($existing) {
1262 $wpdb->update($table, $data, ['id' => $existing->id]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1263 wp_send_json_success(['status' => 'updated', 'title' => $title]);
1264 } else {
1265 $wpdb->insert($table, $data); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1266 wp_send_json_success(['status' => 'inserted', 'title' => $title]);
1267 }
1268
1269 } elseif ($type === 'str') {
1270 global $wpdb;
1271 $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
1272 if (!$str) {
1273 wp_send_json_error('STR not found');
1274 }
1275
1276 $content = "Query: " . $str->query . "\n";
1277 $content .= "Response: " . wp_strip_all_tags($str->response) . "\n";
1278 if (!empty($str->keyword)) {
1279 $content .= "Keywords: " . $str->keyword . "\n";
1280 }
1281 if (!empty($str->intent)) {
1282 $content .= "Intent: " . $str->intent . "\n";
1283 }
1284
1285 if (strlen(trim($content)) < 20) {
1286 wp_send_json_error('No content found to embed');
1287 }
1288
1289 $embedding = $this->generate_embedding($content);
1290
1291 if (empty($embedding) || is_wp_error($embedding)) {
1292 wp_send_json_error('Failed to generate embedding');
1293 }
1294
1295 $table = $wpdb->prefix . "rag_documents";
1296
1297 $existing = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
1298 $wpdb->prepare(
1299 "SELECT id FROM $table WHERE metadata LIKE %s AND source_type = %s",
1300 '%"str_id":' . $str->id . '%',
1301 'str'
1302 ));
1303
1304 $data = [
1305 "title" => $str->query,
1306 "content" => $content,
1307 "embedding" => wp_json_encode($embedding),
1308 "source_type" => 'str',
1309 "source_url" => '',
1310 "file_url" => '',
1311 "status" => 'complete',
1312 "metadata" => wp_json_encode(['str_id' => $str->id]),
1313 "created_at" => current_time('mysql')
1314 ];
1315
1316 if ($existing) {
1317 $wpdb->update($table, $data, ['id' => $existing->id]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1318 } else {
1319 $wpdb->insert($table, $data); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1320 }
1321 wp_send_json_success(['status' => 'processed', 'title' => 'Simple Text Response ID ' . $id]);
1322 }
1323
1324 wp_send_json_error('Invalid item type');
1325 }
1326
1327 public function get_post_meta_context($post_id) {
1328 if (get_option('rag_embed_meta') != '1') {
1329 return "";
1330 }
1331
1332 $meta_keys_str = get_option('rag_embed_meta_keys', '');
1333 $meta_context = "";
1334
1335 if (!empty($meta_keys_str)) {
1336 $keys = array_map('trim', explode(',', $meta_keys_str));
1337 foreach ($keys as $key) {
1338 $value = get_post_meta($post_id, $key, true);
1339 if (!empty($value)) {
1340 if (is_array($value) || is_object($value)) {
1341 $value = json_encode($value);
1342 }
1343 $meta_context .= "\n" . ucfirst(str_replace('_', ' ', $key)) . ": " . $value;
1344 }
1345 }
1346 } else {
1347 $all_meta = get_post_meta($post_id);
1348 foreach ($all_meta as $key => $values) {
1349 if (strpos($key, '_') === 0) continue;
1350 $value = $values[0];
1351 if (!empty($value)) {
1352 $meta_context .= "\n" . ucfirst(str_replace('_', ' ', $key)) . ": " . $value;
1353 }
1354 }
1355 }
1356
1357 return $meta_context;
1358 }
1359
1360 }
1361 }
1362 if ( ! function_exists( 'Qcld_Bot_Rag' ) ) {
1363
1364 function Qcld_Bot_Rag() {
1365 return Qcld_Bot_Rag::instance();
1366 }
1367 }
1368
1369 // fire off the plugin.
1370 Qcld_Bot_Rag();
1371