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