check_api_endpoint(); add_action('wp_ajax_rag_upload_pdf', array($this, 'ajax_rag_upload_pdf')); add_action('wp_ajax_rag_upload_csv', array($this, 'ajax_rag_upload_csv')); add_action('wp_ajax_rag_upload_xaml', array($this, 'ajax_rag_upload_xaml')); add_action('wp_ajax_qcld_rag_manual_sync', array($this, 'ajax_rag_manual_sync')); add_action('wp_ajax_qcld_rag_delete_document', array($this, 'qcld_rag_delete_document_callback')); add_action('wp_ajax_qcld_rag_get_document', array($this, 'qcld_rag_get_document_callback')); add_action('wp_ajax_qcld_rag_update_document', array($this, 'qcld_rag_update_document_callback')); add_action('wp_ajax_qcld_rag_bulk_delete_documents', array($this, 'qcld_rag_bulk_delete_documents_callback')); add_action('wp_ajax_qcld_rag_delete_all_documents', array($this, 'qcld_rag_delete_all_documents_callback')); add_action('wp_ajax_qcld_rag_get_embed_queue', array($this, 'ajax_qcld_rag_get_embed_queue')); add_action('wp_ajax_qcld_rag_process_item', array($this, 'ajax_qcld_rag_process_item')); add_action('save_post', array($this, 'wp_rag_handle_auto_sync_hook'), 10, 3); } public function check_api_endpoint() { if (get_option('qcld_gemini_enabled') == '1') { $this->api_key = get_option('qcld_gemini_api_key'); $this->baseUrl = 'https://generativelanguage.googleapis.com/v1beta'; } else if (get_option('ai_enabled') == '1') { $this->api_key = get_option('open_ai_api_key'); $this->baseUrl = 'https://api.openai.com/v1/'; } else if (get_option('qcld_openrouter_enabled') == '1') { $this->api_key = get_option('qcld_openrouter_api_key'); $this->baseUrl = 'https://openrouter.ai/api/v1/'; } else if (get_option('qcld_grok_enabled') == '1') { $this->api_key = get_option('qcld_grok_api_key'); $this->baseUrl = 'https://api.x.ai/v1/'; } else { $this->api_key = ''; $this->baseUrl = ''; } if (empty($this->api_key)) { return new WP_Error('no_api_key', 'No API key is set.'); } return true; } public function generate_embedding($text) { if (!empty($this->api_key) && get_option('ai_enabled') == '1') { return $this->generate_openai_embedding($text); } else if (!empty($this->api_key) && get_option('qcld_gemini_enabled') == '1') { return $this->generate_gemini_embedding($text); } else if (!empty($this->api_key) && get_option('qcld_openrouter_enabled') == '1' || ($this->baseUrl == 'https://openrouter.ai/api/v1/')) { return $this->generate_openrouter_embedding($text); } else if (!empty($this->api_key) && get_option('qcld_grok_enabled') == '1' || ($this->baseUrl == 'https://api.x.ai/v1/')) { return $this->generate_xai_embedding($text); } } public function generate_xai_embedding( $text, $model = 'text-embedding-3-small' ) { if (empty($this->api_key)) { $this->api_key = get_option('qcld_grok_api_key'); } if (empty($this->api_key)) { return []; } $response = wp_remote_post( "https://api.x.ai/v1/embeddings", [ 'headers' => [ 'Authorization' => "Bearer {$this->api_key}", 'Content-Type' => 'application/json' ], 'body' => wp_json_encode([ 'model' => $model, 'input' => mb_substr($text, 0, 15000) // keep within token limits ]) ] ); if (is_wp_error($response)) return $response; $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['data'][0]['embedding'])) { return $body['data'][0]['embedding']; } if (isset($body['error']['message'])) { return new WP_Error('xai_error', 'xAI Error: ' . $body['error']['message']); } return new WP_Error('invalid_response', 'Invalid response format from xAI API'); } public function generate_openrouter_embedding( $text, $model = 'openai/text-embedding-3-small' ) { if (empty($this->api_key)) { $this->api_key = get_option('qcld_openrouter_api_key'); } if (empty($this->api_key)) { return []; } $response = wp_remote_post( "https://openrouter.ai/api/v1/embeddings", [ 'headers' => [ 'Authorization' => "Bearer {$this->api_key}", 'Content-Type' => 'application/json' ], 'body' => wp_json_encode([ 'model' => $model, 'input' => mb_substr($text, 0, 15000) // keep within token limits ]) ] ); if (is_wp_error($response)) return $response; $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['data'][0]['embedding'])) { return $body['data'][0]['embedding']; } if (isset($body['error']['message'])) { return new WP_Error('openrouter_error', 'OpenRouter Error: ' . $body['error']['message']); } if (isset($body['error']) && is_string($body['error'])) { return new WP_Error('openrouter_error', 'OpenRouter Error: ' . $body['error']); } return new WP_Error('invalid_response', 'Invalid response format from OpenRouter API'); } public function generate_gemini_embedding($text, $model = 'text-embedding-004') { $url = "{$this->baseUrl}/models/{$model}:embedContent?key={$this->api_key}"; $data = [ 'model' => "models/{$model}", 'content' => [ 'parts' => [ ['text' => $text] ] ] ]; $response_raw = wp_remote_post( $url, array( 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $data ), 'timeout' => 30, ) ); if ( is_wp_error( $response_raw ) ) { return []; } $httpCode = wp_remote_retrieve_response_code( $response_raw ); if ( $httpCode !== 200 ) { return []; } $result = json_decode( wp_remote_retrieve_body( $response_raw ), true ); if (isset($result['embedding']['values'])) { return $result['embedding']['values']; } // throw new Exception("No embedding in response: $response"); return []; } // Generate OpenAI embedding for given text public function generate_openai_embedding($text) { $response = wp_remote_post( "https://api.openai.com/v1/embeddings", [ 'headers' => [ 'Authorization' => "Bearer {$this->api_key}", 'Content-Type' => 'application/json' ], 'body' => wp_json_encode([ 'model' => 'text-embedding-3-large', 'input' => mb_substr($text, 0, 15000) // keep within token limits ]) ] ); if (is_wp_error($response)) return []; $body = json_decode(wp_remote_retrieve_body($response), true); return $body['data'][0]['embedding'] ?? []; } // AJAX handler for PDF upload public function ajax_rag_upload_pdf() { check_ajax_referer('rag_upload_nonce', 'nonce'); if (empty($_FILES['rag_pdf'])) { wp_send_json_error(['message' => 'No PDF files uploaded']); } ob_start(); $this->wp_rag_process_pdf_upload(); $output = ob_get_clean(); wp_send_json_success(['message' => 'PDF processing complete', 'output' => $output]); } // AJAX handler for XAML upload public function ajax_rag_upload_xaml() { check_ajax_referer('rag_upload_nonce', 'nonce'); if (empty($_FILES['rag_xaml'])) { wp_send_json_error(['message' => 'No XAML files uploaded']); } ob_start(); $this->wp_rag_process_xaml_upload(); $output = ob_get_clean(); wp_send_json_success(['message' => 'XAML processing complete', 'output' => $output]); } // AJAX handler for CSV upload public function ajax_rag_upload_csv() { check_ajax_referer('rag_upload_nonce', 'nonce'); if (empty($_FILES['rag_csv'])) { wp_send_json_error(['message' => 'No CSV files uploaded']); } // ob_start(); $this->wp_rag_process_csv_upload(); // $output = ob_get_clean(); // wp_send_json_success(['message' => 'CSV processing complete', 'output' => $output]); } // CSV processing method public function wp_rag_process_csv_upload() { if (empty($_FILES['rag_csv']['name'][0])) { echo "

No CSV selected.

"; return; } require_once(ABSPATH . 'wp-admin/includes/file.php'); $uploaded_files = $_FILES['rag_csv']; foreach ($uploaded_files['name'] as $index => $filename) { $file_array = [ 'name' => $uploaded_files['name'][$index], 'type' => $uploaded_files['type'][$index], 'tmp_name' => $uploaded_files['tmp_name'][$index], 'error' => $uploaded_files['error'][$index], 'size' => $uploaded_files['size'][$index], ]; $upload = wp_handle_upload($file_array, ['test_form' => false]); if (isset($upload['error'])) { echo "

Error uploading: " . esc_html($filename) . "

"; continue; } $file_url = $upload['url']; $file_path = $upload['file']; echo "

Uploaded: " . esc_html($filename) . "

"; $handle = fopen($file_path, 'r'); // phpcs:ignore WordPress.WP.AlternativeFunctions if ($handle === false) { echo "

Failed to open CSV file

"; continue; } $header = fgetcsv($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions if ($header === false) { echo "

Empty CSV file

"; fclose($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions continue; } echo "

Columns: " . esc_html( implode(', ', $header) ) . "

"; global $wpdb; $table = $wpdb->prefix . "rag_documents"; $row_count = 0; $success_count = 0; while (($row = fgetcsv($handle)) !== false) { // phpcs:ignore WordPress.WP.AlternativeFunctions $row_count++; $content = ''; foreach ($header as $i => $col_name) { if (isset($row[$i])) { $content .= "$col_name: " . $row[$i] . "\n"; } } if (strlen(trim($content)) < 10) { continue; } $embedding = $this->generate_embedding($content); if (empty($embedding)) { echo "

Failed embedding for row " . esc_html($row_count) . "

"; continue; } $title = !empty($row[0]) ? substr($row[0], 0, 100) : "CSV Row $row_count"; $result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $table, [ 'title' => sanitize_text_field($title), 'content' => $content, 'embedding' => wp_json_encode($embedding), 'source_type' => 'csv', 'source_url' => $file_url, 'file_url' => $file_url, 'status' => 'complete', 'metadata' => wp_json_encode(['filename' => $filename, 'row' => $row_count]), 'created_at' => current_time('mysql') ]); if ($result !== false) { $success_count++; } else { echo "

DB error row " . esc_html($row_count) . ": " . esc_html($wpdb->last_error) . "

"; } } fclose($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions echo "

✓ Processed " . esc_html($success_count) . " of " . esc_html($row_count) . " rows

"; } echo "

CSV Processing Complete!

"; } public function wp_rag_process_pdf_upload() { if (empty($_FILES['rag_pdf']['name'][0])) { echo "

No PDF selected.

"; return; } require_once(ABSPATH . 'wp-admin/includes/file.php'); $uploaded_files = $_FILES['rag_pdf']; foreach ($uploaded_files['name'] as $index => $filename) { // Upload to WP Media $file_array = [ 'name' => $uploaded_files['name'][$index], 'type' => $uploaded_files['type'][$index], 'tmp_name' => $uploaded_files['tmp_name'][$index], 'error' => $uploaded_files['error'][$index], 'size' => $uploaded_files['size'][$index], ]; $upload = wp_handle_upload($file_array, ['test_form' => false]); if (isset($upload['error'])) { echo "

Error uploading: " . esc_html($filename) . "

"; continue; } $file_url = $upload['url']; $file_path = $upload['file']; echo "

Uploaded: " . esc_html($filename) . "

"; // Extract PDF text (uses Smalot/PdfParser) if (!class_exists('\Smalot\PdfParser\Parser')) { echo "

PDF parser missing! Install `smalot/pdfparser`.

"; return; } $parser = new \Smalot\PdfParser\Parser(); $pdf = $parser->parseFile($file_path); $text = $pdf->getText(); echo "

Extracted text length: " . esc_html(strlen($text)) . "

"; // Generate Embedding $embedding = $this->generate_embedding($text); if (empty($embedding)) { echo "

Failed to generate embedding for: " . esc_html($filename) . "

"; continue; } // Save to DB global $wpdb; $table = $wpdb->prefix . "rag_documents"; $result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $table, [ 'title' => sanitize_text_field($filename), 'content' => $text, 'embedding' => wp_json_encode($embedding), 'source_type' => 'pdf', 'source_url' => $file_url, 'file_url' => $file_url, 'status' => 'complete', 'metadata' => wp_json_encode(['size' => $uploaded_files['size'][$index]]), 'created_at' => current_time('mysql') ]); if ($result === false) { echo "

Database error: " . esc_html($wpdb->last_error) . "

"; } else { echo "

✓ Saved PDF embedding for: " . esc_html($filename) . " (ID: " . esc_html($wpdb->insert_id) . ")

"; } } echo "

PDF Processing Complete!

"; } // XAML processing method public function wp_rag_process_xaml_upload() { if (empty($_FILES['rag_xaml']['name'][0])) { echo "

No XAML selected.

"; return; } require_once(ABSPATH . 'wp-admin/includes/file.php'); $uploaded_files = $_FILES['rag_xaml']; foreach ($uploaded_files['name'] as $index => $filename) { $file_array = [ 'name' => $uploaded_files['name'][$index], 'type' => $uploaded_files['type'][$index], 'tmp_name' => $uploaded_files['tmp_name'][$index], 'error' => $uploaded_files['error'][$index], 'size' => $uploaded_files['size'][$index], ]; $upload = wp_handle_upload($file_array, [ 'test_form' => false, 'test_type' => false, // Bypass mime type check ]); if (isset($upload['error'])) { echo "

Error uploading " . esc_html($filename) . ": " . esc_html($upload['error']) . "

"; continue; } $file_url = $upload['url']; $file_path = $upload['file']; echo "

Uploaded: " . esc_html($filename) . "

"; // Read XAML/XML content global $wp_filesystem; if ( empty( $wp_filesystem ) ) { require_once ABSPATH . '/wp-admin/includes/file.php'; WP_Filesystem(); } $xml_content = $wp_filesystem->get_contents( $file_path ); if (empty($xml_content)) { echo "

Failed to read file or file is empty: " . esc_html($filename) . "

"; continue; } // Attempt to parse as XML $xml = simplexml_load_string($xml_content, 'SimpleXMLElement', LIBXML_NOCDATA); $items_to_process = []; if ($xml && isset($xml->channel->item)) { // It's likely a WordPress export (WXR) file echo "

Detected WordPress Export format. Extracting items...

"; foreach ($xml->channel->item as $item) { $title = (string)$item->title; $namespaces = $item->getNameSpaces(true); $content = ""; if (isset($namespaces['content'])) { $content = (string)$item->children($namespaces['content'])->encoded; } else { $content = (string)$item->description; } if (!empty($content)) { $items_to_process[] = [ 'title' => !empty($title) ? $title : $filename, 'content' => $content, 'source_url' => (string)$item->link ]; } } } else { // Treat as generic text/XML $items_to_process[] = [ 'title' => $filename, 'content' => $xml_content, 'source_url' => $file_url ]; } foreach ($items_to_process as $item_data) { $clean_content = $this->clean_rag_content($item_data['content']); if (empty($clean_content) || strlen($clean_content) < 20) { continue; } // Generate Embedding $embedding = $this->generate_embedding($clean_content); if (empty($embedding)) { echo "

Failed to generate embedding for item: " . esc_html($item_data['title']) . "

"; continue; } // Save to DB global $wpdb; $table = $wpdb->prefix . "rag_documents"; $result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $table, [ 'title' => sanitize_text_field($item_data['title']), 'content' => $clean_content, 'embedding' => wp_json_encode($embedding), 'source_type' => 'xaml', 'source_url' => $item_data['source_url'] ? $item_data['source_url'] : $file_url, 'file_url' => $file_url, 'status' => 'complete', 'metadata' => wp_json_encode(['size' => strlen($clean_content)]), 'created_at' => current_time('mysql') ]); if ($result === false) { echo "

Database error for item " . esc_html($item_data['title']) . ": " . esc_html($wpdb->last_error) . "

"; } else { echo "

✓ Saved embedding for: " . esc_html($item_data['title']) . " (ID: " . esc_html($wpdb->insert_id) . ")

"; } } } echo "

XAML Processing Complete!

"; } public function wp_rag_embed_all_documents() { $apiKey = get_option('open_ai_api_key'); // Replace with option if needed global $wpdb; $posts = get_posts([ 'post_type' => ['post', 'page'], 'posts_per_page' => -1 ]); echo ""; echo "Completed!"; } public function wp_rag_embed_all_sources() { // $apiKey = get_option('open_ai_api_key'); global $wpdb; $post_types = []; if (get_option('rag_embed_pages') == '1') { $post_types[] = 'page'; } if (get_option('rag_embed_posts') == '1') { $post_types[] = 'post'; } $cpts = get_option('rag_embed_cpts', []); if (!empty($cpts) && is_array($cpts)) { $post_types = array_merge($post_types, $cpts); } $table = $wpdb->prefix . "rag_documents"; $updated_count = 0; $inserted_count = 0; $skipped_count = 0; echo ""; echo "

All Selected Sources Processed!

"; echo "

Summary:

"; echo ""; } public function wp_rag_create_embedding($text, $apiKey) { $response = $this->generate_embedding($text); return $response; } public function ajax_rag_manual_sync() { check_ajax_referer('wp_chatbot', 'nonce'); $doc_id = isset($_POST['id']) ? intval($_POST['id']) : 0; if (!$doc_id) { wp_send_json_error(['message' => 'Invalid document ID']); } global $wpdb; $table = $wpdb->prefix . 'rag_documents'; $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 if (!$doc) { wp_send_json_error(['message' => 'Document not found']); } // Try to extract post/product info $post_id = 0; if (!empty($doc->metadata)) { $metadata = json_decode($doc->metadata, true); if (isset($metadata['post_id'])) { $post_id = intval($metadata['post_id']); } } if (!$post_id && ($doc->source_type === 'page' || $doc->source_type === 'post' || $doc->source_type === 'xaml')) { $post_id = url_to_postid($doc->source_url); } if (!$post_id) { wp_send_json_error(['message' => 'Could not determine source post for manual sync']); } $result = $this->wp_rag_sync_post($post_id, true); if (is_wp_error($result)) { wp_send_json_error(['message' => $result->get_error_message()]); } wp_send_json_success(['message' => 'Document synced successfully!']); } public function qcld_rag_delete_document_callback() { check_ajax_referer('wp_chatbot', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized'); } global $wpdb; $id = intval($_POST['id']); $table_name = $wpdb->prefix . 'rag_documents'; $deleted = $wpdb->delete($table_name, array('id' => $id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ($deleted) { wp_send_json_success('Document deleted successfully.'); } else { wp_send_json_error('Failed to delete document.'); } } public function qcld_rag_bulk_delete_documents_callback() { check_ajax_referer('wp_chatbot', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized'); } if (empty($_POST['ids']) || !is_array($_POST['ids'])) { wp_send_json_error('No documents selected.'); } global $wpdb; $ids = array_map('intval', $_POST['ids']); $table_name = $wpdb->prefix . 'rag_documents'; $ids_string = implode(',', $ids); $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 if ($deleted !== false) { wp_send_json_success('Selected documents deleted successfully.'); } else { wp_send_json_error('Failed to delete selected documents.'); } } public function qcld_rag_delete_all_documents_callback() { check_ajax_referer('wp_chatbot', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized'); } global $wpdb; $table_name = $wpdb->prefix . 'rag_documents'; $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 // Some DBs might not support TRUNCATE on tables with foreign keys or other constraints, // though rag_documents is likely simple. Fallback to DELETE. if ($deleted === false) { $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 } if ($deleted !== false) { wp_send_json_success('All documents deleted successfully.'); } else { wp_send_json_error('Failed to delete all documents.'); } } public function qcld_rag_get_document_callback() { check_ajax_referer('wp_chatbot', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized'); } global $wpdb; $id = intval($_POST['id']); $table_name = $wpdb->prefix . 'rag_documents'; $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 if ($document) { wp_send_json_success($document); } else { wp_send_json_error('Document not found.'); } } public function qcld_rag_update_document_callback() { check_ajax_referer('wp_chatbot', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized'); } global $wpdb; $id = intval(wp_unslash($_POST['id'])); $title = sanitize_text_field(wp_unslash($_POST['title'])); $content = sanitize_textarea_field(wp_unslash($_POST['content'])); $table_name = $wpdb->prefix . 'rag_documents'; // Re-generate embedding if content changed $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 $update_data = array( 'title' => $title, 'content' => $content, 'status' => 'complete' ); if ($old_content !== $content) { $embedding = $this->generate_embedding($content); if (!empty($embedding)) { $update_data['embedding'] = wp_json_encode($embedding); } else { $update_data['status'] = 'error'; } } $updated = $wpdb->update($table_name, $update_data, array('id' => $id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ($updated !== false) { wp_send_json_success('Document updated successfully.'); } else { wp_send_json_error('Failed to update document.'); } } public function wp_rag_sync_post($post_id, $force = false) { $post = get_post($post_id); if (!$post) return new WP_Error('invalid_post', 'Post not found'); $title = $post->post_title; $url = get_permalink($post_id); $content = "Title: " . $title . "\n"; $content .= "Date: " . $post->post_date . "\n"; $main_content = strip_shortcodes($post->post_content); $main_content = wp_strip_all_tags($main_content); $content .= $main_content; // Specific handling for WooCommerce Products if ($post->post_type === 'product' && class_exists('WC_Product') && function_exists('wc_get_product')) { $_product = wc_get_product($post_id); if ($_product) { $price = $_product->get_price(); $currency = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol() : '$'; $content .= "\nPrice: " . $currency . $price; // Add description if main content is empty (sometimes WC uses short description) if (empty($main_content) && method_exists($_product, 'get_short_description')) { $content .= "\nDescription: " . wp_strip_all_tags($_product->get_short_description()); } } } if (empty(trim($main_content)) && !($post->post_type === 'product')) { return new WP_Error('empty_content', 'No content found to embed'); } // Generate Embedding $embedding = $this->generate_embedding($content); if (empty($embedding)) { return new WP_Error('embedding_failed', 'Failed to generate embedding'); } global $wpdb; $table = $wpdb->prefix . "rag_documents"; // Check if it already exists (by source_url or custom metadata if we had it) $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 if ($existing) { $result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $table, [ 'title' => sanitize_text_field($title), 'content' => $content, 'embedding' => wp_json_encode($embedding), 'status' => 'complete', 'created_at' => current_time('mysql') ], ['id' => $existing->id]); } else { $result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $table, [ 'title' => sanitize_text_field($title), 'content' => $content, 'embedding' => wp_json_encode($embedding), 'source_type' => ($post->post_type === 'page' || $post->post_type === 'post') ? $post->post_type : 'xaml', 'source_url' => $url, 'file_url' => $url, 'status' => 'complete', 'metadata' => wp_json_encode(['post_id' => $post_id, 'post_type' => $post->post_type]), 'created_at' => current_time('mysql') ]); } return $result; } public function wp_rag_handle_auto_sync_hook($post_id, $post, $update) { // Only run if auto-sync is enabled if (get_option('rag_auto_sync_enabled') != '1') { return; } // Avoid autosaves and revisions if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if (wp_is_post_revision($post_id)) return; if ($post->post_status != 'publish') return; // Check if post type is enabled in general embedding settings $is_allowed = false; if ($post->post_type === 'page' && get_option('rag_embed_pages') == '1') { $is_allowed = true; } elseif ($post->post_type === 'post' && get_option('rag_embed_posts') == '1') { $is_allowed = true; } else { $cpts = get_option('rag_embed_cpts', []); if (is_array($cpts) && in_array($post->post_type, $cpts)) { $is_allowed = true; } } if (!$is_allowed) { return; } $this->wp_rag_sync_post($post_id); } public function clean_rag_content($text) { if (empty($text)) return ""; // Remove WordPress block comments like $text = preg_replace('//s', '', $text); // Remove generic HTML comments $text = preg_replace('//s', '', $text); // Strip HTML tags $text = wp_strip_all_tags($text); // Decode HTML entities $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); // Normalize whitespace $text = preg_replace('/\s+/', ' ', $text); return trim($text); } public function run_rag_search($user_query, $top_k = 3) { global $wpdb; $table = $wpdb->prefix . "rag_documents"; // Get all embeddings and texts $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 if (empty($rows)) { return "No knowledge base found."; } // Step 1: Get embedding for user query $query_vector = $this->generate_embedding($user_query); if (empty($query_vector)) { return ""; } // Step 2: Compute cosine similarity $ranked = []; foreach ($rows as $row) { $doc_embedding = json_decode($row['embedding'], true); if (!is_array($doc_embedding) || empty($doc_embedding)) { continue; } $sim = $this->cosine_similarity($query_vector, $doc_embedding); $ranked[] = ["score" => $sim, "text" => $row['content']]; } // Step 3: Sort by similarity usort($ranked, function ($a, $b) { return $a['score'] < $b['score'] ? 1 : -1; }); // Select top k documents $top_docs = array_slice($ranked, 0, $top_k); $context = ""; foreach ($top_docs as $doc) { $context .= $doc["text"] . "\n\n"; } return trim($context); } private function cosine_similarity($vecA, $vecB) { if (!is_array($vecA) || !is_array($vecB) || count($vecA) !== count($vecB)) { return 0.0; } $dot = 0.0; $normA = 0.0; $normB = 0.0; $len = count($vecA); for ($i = 0; $i < $len; $i++) { $dot += $vecA[$i] * $vecB[$i]; $normA += $vecA[$i] ** 2; $normB += $vecB[$i] ** 2; } if ($normA == 0 || $normB == 0) { return 0.0; } return $dot / (sqrt($normA) * sqrt($normB)); } public function ajax_qcld_rag_get_embed_queue() { check_ajax_referer('wp_chatbot', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized'); } global $wpdb; $queue = []; // Posts, Pages, CPTs $post_types = []; if (get_option('rag_embed_pages') == '1') { $post_types[] = 'page'; } if (get_option('rag_embed_posts') == '1') { $post_types[] = 'post'; } $cpts = get_option('rag_embed_cpts', []); if (!empty($cpts) && is_array($cpts)) { $post_types = array_merge($post_types, $cpts); } if (!empty($post_types)) { $posts = get_posts([ 'post_type' => $post_types, 'posts_per_page' => -1, 'post_status' => 'publish', 'fields' => 'ids' ]); foreach ($posts as $post_id) { $queue[] = ['id' => $post_id, 'type' => 'post']; } } // Simple Text Responses if (get_option('rag_embed_str') == '1') { $str_ids = $wpdb->get_col("SELECT id FROM {$wpdb->prefix}wpbot_response"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching foreach ($str_ids as $str_id) { $queue[] = ['id' => $str_id, 'type' => 'str']; } } wp_send_json_success($queue); } public function ajax_qcld_rag_process_item() { check_ajax_referer('wp_chatbot', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized'); } $id = intval($_POST['item_id']); $type = sanitize_text_field($_POST['item_type']); if ($type === 'post') { $p = get_post($id); if (!$p) { wp_send_json_error('Post not found'); } global $wpdb; $table = $wpdb->prefix . "rag_documents"; $apiKey = get_option('open_ai_api_key'); $title = $p->post_title; $content = "Title: " . $title . "\n"; $main_content = strip_shortcodes($p->post_content); $main_content = wp_strip_all_tags($main_content); $content .= $main_content; // Add Post Meta Context $content .= $this->get_post_meta_context($id); if ($p->post_type === 'product' && class_exists('WC_Product')) { $_product = wc_get_product($p->ID); if ($_product) { $price = $_product->get_price(); $currency = get_woocommerce_currency_symbol(); $content .= "\nPrice: " . $currency . $price; if (empty(trim($main_content))) { $content .= "\nDescription: " . wp_strip_all_tags($_product->get_short_description()); } $content .= "\nProduct Link: " . get_permalink($p->ID); $content .= "\nProduct ID: " . $p->ID; } } if (strlen(trim($content)) < 20) { wp_send_json_success(['status' => 'skipped', 'title' => $title]); } $embedding = $this->wp_rag_create_embedding($content, $apiKey); if (empty($embedding) || is_wp_error($embedding)) { $error_msg = is_wp_error($embedding) ? $embedding->get_error_message() : 'Failed to generate embedding'; wp_send_json_error($error_msg); } $existing = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->prepare( "SELECT id FROM $table WHERE metadata LIKE %s AND source_type = %s", '%"post_id":' . $p->ID . '%', $p->post_type )); $data = [ "title" => $p->post_title, "content" => $content, "embedding" => wp_json_encode($embedding), "source_type" => $p->post_type, "source_url" => get_permalink($p->ID), "file_url" => get_permalink($p->ID), "status" => 'complete', "metadata" => wp_json_encode(['post_id' => $p->ID]), "created_at" => current_time('mysql') ]; if ($existing) { $wpdb->update($table, $data, ['id' => $existing->id]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching wp_send_json_success(['status' => 'updated', 'title' => $title]); } else { $wpdb->insert($table, $data); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery wp_send_json_success(['status' => 'inserted', 'title' => $title]); } } elseif ($type === 'str') { global $wpdb; $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 if (!$str) { wp_send_json_error('STR not found'); } $content = "Query: " . $str->query . "\n"; $content .= "Response: " . wp_strip_all_tags($str->response) . "\n"; if (!empty($str->keyword)) { $content .= "Keywords: " . $str->keyword . "\n"; } if (!empty($str->intent)) { $content .= "Intent: " . $str->intent . "\n"; } if (strlen(trim($content)) < 20) { wp_send_json_error('No content found to embed'); } $embedding = $this->generate_embedding($content); if (empty($embedding) || is_wp_error($embedding)) { wp_send_json_error('Failed to generate embedding'); } $table = $wpdb->prefix . "rag_documents"; $existing = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->prepare( "SELECT id FROM $table WHERE metadata LIKE %s AND source_type = %s", '%"str_id":' . $str->id . '%', 'str' )); $data = [ "title" => $str->query, "content" => $content, "embedding" => wp_json_encode($embedding), "source_type" => 'str', "source_url" => '', "file_url" => '', "status" => 'complete', "metadata" => wp_json_encode(['str_id' => $str->id]), "created_at" => current_time('mysql') ]; if ($existing) { $wpdb->update($table, $data, ['id' => $existing->id]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching } else { $wpdb->insert($table, $data); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery } wp_send_json_success(['status' => 'processed', 'title' => 'Simple Text Response ID ' . $id]); } wp_send_json_error('Invalid item type'); } public function get_post_meta_context($post_id) { if (get_option('rag_embed_meta') != '1') { return ""; } $meta_keys_str = get_option('rag_embed_meta_keys', ''); $meta_context = ""; if (!empty($meta_keys_str)) { $keys = array_map('trim', explode(',', $meta_keys_str)); foreach ($keys as $key) { $value = get_post_meta($post_id, $key, true); if (!empty($value)) { if (is_array($value) || is_object($value)) { $value = json_encode($value); } $meta_context .= "\n" . ucfirst(str_replace('_', ' ', $key)) . ": " . $value; } } } else { $all_meta = get_post_meta($post_id); foreach ($all_meta as $key => $values) { if (strpos($key, '_') === 0) continue; $value = $values[0]; if (!empty($value)) { $meta_context .= "\n" . ucfirst(str_replace('_', ' ', $key)) . ": " . $value; } } } return $meta_context; } } } if ( ! function_exists( 'Qcld_Bot_Rag' ) ) { function Qcld_Bot_Rag() { return Qcld_Bot_Rag::instance(); } } // fire off the plugin. Qcld_Bot_Rag();