File: /home/d5123/myboofola_com/wp-content/plugins/chatbot/includes/class-qcld-bot-rag.php
<?php
if (!defined('ABSPATH')) exit; // Exit if accessed directly
if ( ! class_exists( 'Qcld_Bot_Rag' ) ) {
class Qcld_Bot_Rag{
private $api_key;
private $baseUrl;
/**
* @var self
*/
private static $_instance = null;
/**
* @return self
*/
public static function instance() {
if ( is_null( self::$_instance ) ) {
self::$_instance = new self();
}
return self::$_instance;
}
public function __construct() {
$this->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 "<p>No CSV selected.</p>";
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 "<p>Error uploading: " . esc_html($filename) . "</p>";
continue;
}
$file_url = $upload['url'];
$file_path = $upload['file'];
echo "<p>Uploaded: " . esc_html($filename) . "</p>";
$handle = fopen($file_path, 'r'); // phpcs:ignore WordPress.WP.AlternativeFunctions
if ($handle === false) {
echo "<p style='color:red;'>Failed to open CSV file</p>";
continue;
}
$header = fgetcsv($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions
if ($header === false) {
echo "<p style='color:red;'>Empty CSV file</p>";
fclose($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions
continue;
}
echo "<p>Columns: " . esc_html( implode(', ', $header) ) . "</p>";
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 "<p style='color:red;'>Failed embedding for row " . esc_html($row_count) . "</p>";
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 "<p style='color:red;'>DB error row " . esc_html($row_count) . ": " . esc_html($wpdb->last_error) . "</p>";
}
}
fclose($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions
echo "<p style='color:green;'>✓ Processed " . esc_html($success_count) . " of " . esc_html($row_count) . " rows</p>";
}
echo "<h3>CSV Processing Complete!</h3>";
}
public function wp_rag_process_pdf_upload() {
if (empty($_FILES['rag_pdf']['name'][0])) {
echo "<p>No PDF selected.</p>";
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 "<p>Error uploading: " . esc_html($filename) . "</p>";
continue;
}
$file_url = $upload['url'];
$file_path = $upload['file'];
echo "<p>Uploaded: " . esc_html($filename) . "</p>";
// Extract PDF text (uses Smalot/PdfParser)
if (!class_exists('\Smalot\PdfParser\Parser')) {
echo "<p>PDF parser missing! Install `smalot/pdfparser`.</p>";
return;
}
$parser = new \Smalot\PdfParser\Parser();
$pdf = $parser->parseFile($file_path);
$text = $pdf->getText();
echo "<p>Extracted text length: " . esc_html(strlen($text)) . "</p>";
// Generate Embedding
$embedding = $this->generate_embedding($text);
if (empty($embedding)) {
echo "<p style='color:red;'>Failed to generate embedding for: " . esc_html($filename) . "</p>";
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 "<p style='color:red;'>Database error: " . esc_html($wpdb->last_error) . "</p>";
} else {
echo "<p style='color:green;'>✓ Saved PDF embedding for: " . esc_html($filename) . " (ID: " . esc_html($wpdb->insert_id) . ")</p>";
}
}
echo "<h3>PDF Processing Complete!</h3>";
}
// XAML processing method
public function wp_rag_process_xaml_upload() {
if (empty($_FILES['rag_xaml']['name'][0])) {
echo "<p>No XAML selected.</p>";
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 "<p>Error uploading " . esc_html($filename) . ": " . esc_html($upload['error']) . "</p>";
continue;
}
$file_url = $upload['url'];
$file_path = $upload['file'];
echo "<p>Uploaded: " . esc_html($filename) . "</p>";
// 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 "<p style='color:red;'>Failed to read file or file is empty: " . esc_html($filename) . "</p>";
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 "<p>Detected WordPress Export format. Extracting items...</p>";
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 "<p style='color:red;'>Failed to generate embedding for item: " . esc_html($item_data['title']) . "</p>";
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 "<p style='color:red;'>Database error for item " . esc_html($item_data['title']) . ": " . esc_html($wpdb->last_error) . "</p>";
} else {
echo "<p style='color:green;'>✓ Saved embedding for: " . esc_html($item_data['title']) . " (ID: " . esc_html($wpdb->insert_id) . ")</p>";
}
}
}
echo "<h3>XAML Processing Complete!</h3>";
}
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 "<ul>";
foreach ($posts as $p) {
$content = wp_strip_all_tags($p->post_content);
if (strlen($content) < 20) continue;
$embedding = $this->wp_rag_create_embedding($content, $apiKey);
$wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->prefix . "rag_documents",
[
"title" => $p->post_title,
"content" => $content,
"embedding" => wp_json_encode($embedding)
]
);
echo "<li>Embedded: " . esc_html($p->post_title) . "</li>";
flush();
}
echo "</ul>";
echo "<strong>Completed!</strong>";
}
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 "<ul>";
if (!empty($post_types)) {
$posts = get_posts([
'post_type' => $post_types,
'posts_per_page' => -1,
'post_status' => 'publish'
]);
foreach ($posts as $p) {
$title = $p->post_title;
$content = "Title: " . $title . "\n";
$content .= "Date: " . $p->post_date . "\n";
$main_content = strip_shortcodes($p->post_content);
$main_content = wp_strip_all_tags($main_content);
$content .= $main_content;
// Specific handling for WooCommerce Products
if ($p->post_type === 'product' && class_exists('WC_Product') && function_exists('wc_get_product')) {
$_product = wc_get_product($p->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
if (empty(trim($main_content)) && method_exists($_product, 'get_short_description')) {
$content .= "\nDescription: " . wp_strip_all_tags($_product->get_short_description());
}
}
}
if (strlen(trim($content)) < 20) {
$skipped_count++;
continue;
}
$embedding = $this->generate_embedding($content);
if (empty($embedding)) {
echo "<li style='color:red;'>Failed to generate embedding for: " . esc_html($p->post_title) . "</li>";
continue;
}
$table = $wpdb->prefix . "rag_documents";
// Check if this post already exists in the database
$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) {
// Update existing record
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$table,
$data,
['id' => $existing->id]
);
echo "<li style='color:blue;'>✓ Updated: " . esc_html($p->post_title) . " (" . esc_html($p->post_type) . ")</li>";
$updated_count++;
} else {
// Insert new record
$wpdb->insert($table, $data); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
echo "<li style='color:green;'>✓ Embedded: " . esc_html($p->post_title) . " (" . esc_html($p->post_type) . ")</li>";
$inserted_count++;
}
if (function_exists('flush')) {
@flush();
}
if (function_exists('ob_flush')) {
@ob_flush();
}
}
}
// Simple Text Responses Embedding
if (get_option('rag_embed_str') == '1') {
$str_table = $wpdb->prefix . 'wpbot_response';
$str_results = $wpdb->get_results("SELECT * FROM $str_table"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
if (!empty($str_results)) {
foreach ($str_results as $str) {
$content = "Query: " . $str->query . "\n";
$content .= "Response: " . wp_strip_all_tags($str->response) . "\n";
if (!empty($str->keyword)) {
$content .= "Keywords: " . $str->keyword;
}
if (strlen(trim($content)) < 10) {
$skipped_count++;
continue;
}
$embedding = $this->generate_embedding($content);
if (empty($embedding)) {
echo "<li style='color:red;'>Failed to generate embedding for STR: " . esc_html($str->query) . "</li>";
continue;
}
// Check if this STR already exists in the RAG database
$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" => admin_url('admin.php?page=simple-text-response&action=edit&query=' . $str->id),
"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
echo "<li style='color:blue;'>✓ Updated STR: " . esc_html($str->query) . "</li>";
$updated_count++;
} else {
$wpdb->insert($table, $data); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
echo "<li style='color:green;'>✓ Embedded STR: " . esc_html($str->query) . "</li>";
$inserted_count++;
}
if (function_exists('flush')) { @flush(); }
if (function_exists('ob_flush')) { @ob_flush(); }
}
}
}
echo "</ul>";
echo "<h3>All Selected Sources Processed!</h3>";
echo "<p><strong>Summary:</strong></p>";
echo "<ul>";
echo "<li>New entries created: <strong>" . esc_html($inserted_count) . "</strong></li>";
echo "<li>Existing entries updated: <strong style='color:blue;'>" . esc_html($updated_count) . "</strong></li>";
echo "<li>Skipped (too short): <strong>" . esc_html($skipped_count) . "</strong></li>";
echo "</ul>";
}
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 <!-- wp:paragraph -->
$text = preg_replace('/<!--\s*\/?[a-z0-9_-]+:[a-z0-9_-]+\s*({.*?})?\s*-->/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();