PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.4.6
MxChat – AI Chatbot & Content Generation for WordPress v2.4.6
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | includes/class-mxchat-integrator.php +597 -4073 3.2.52.4.6 View file →
@@ -10,40 +10,10 @@
10 10 private $fallbackResponse;
11 11 private $productCardHtml;
12 12 private $word_handler;
13 13 private $last_similarity_analysis = null;
14 - private $current_valid_urls = [];
15 - private $last_vectorstore_error = null;
16 - private $is_streaming = false; // ADDED: Track if current request is streaming
17 - private $streaming_headers_sent = false; // Track if streaming headers have been sent
18 14
19 -/**
20 - * Setup streaming headers - call this right before actually streaming
21 - * This delays header setup to allow actions/forms to return JSON responses
22 - */
23 -private function setup_streaming_headers() {
24 - if ($this->streaming_headers_sent || headers_sent()) {
25 - return false;
26 - }
27 15
28 - // Disable output buffering
29 - while (ob_get_level()) {
30 - ob_end_flush();
31 - }
32 -
33 - // Set headers for SSE
34 - header('Content-Type: text/event-stream');
35 - header('Cache-Control: no-cache');
36 - header('Connection: keep-alive');
37 - header('X-Accel-Buffering: no');
38 -
39 - ob_implicit_flush(true);
40 - flush();
41 -
42 - $this->streaming_headers_sent = true;
43 - return true;
44 -}
45 -
46 16 /**
47 17 * Class constructor
48 18 */
49 19 public function __construct() {
@@ -111,28 +81,13 @@
111 81 // Add chat mode checking actions
112 82 add_action('wp_ajax_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
113 83 add_action('wp_ajax_nopriv_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
114 84
115 - // Nonce refresh for page-cache compatibility (WP Rocket, LiteSpeed, etc.)
116 - add_action('wp_ajax_mxchat_refresh_nonce', array($this, 'mxchat_refresh_nonce'));
117 - add_action('wp_ajax_nopriv_mxchat_refresh_nonce', array($this, 'mxchat_refresh_nonce'));
118 -
119 - // Auto-email transcript action
120 - add_action('mxchat_send_delayed_transcript', array($this, 'mxchat_send_delayed_transcript'), 10, 1);
121 -
122 85 add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
123 86
124 87
125 88 }
126 89
127 -/**
128 - * Return a fresh nonce so cached pages can replace the stale one.
129 - */
130 -public function mxchat_refresh_nonce() {
131 - nocache_headers();
132 - wp_send_json_success(array('nonce' => wp_create_nonce('mxchat_chat_nonce')));
133 -}
134 -
135 90 // In your core plugin's check_actions_for_addons method:
136 91 public function check_actions_for_addons($default, $message, $user_id, $session_id) {
137 92 //error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
138 93
@@ -155,22 +110,8 @@
155 110 wp_die();
156 111 }
157 112
158 113 $session_id = sanitize_text_field($_POST['session_id']);
159 -
160 - // SECURITY FIX: Verify session ownership before retrieving data
161 - // If IP/user changed, signal frontend to reset session instead of blocking
162 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
163 -
164 - // Check if this session has an owner recorded
165 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
166 -
167 - // Update session owner if it changed (e.g. IP changed due to network switch)
168 - // The session ID itself is the authentication — if the client has it, they own it
169 - if (!$session_owner || $session_owner !== $current_user_identifier) {
170 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
171 - }
172 -
173 114 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
174 115 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
175 116
176 117 if (empty($history)) {
@@ -187,25 +128,11 @@
187 128 'chat_mode' => $chat_mode
188 129 ]);
189 130 wp_die();
190 131 }
191 -private function mxchat_fetch_conversation_history_for_ai($session_id, $session_start_timestamp = 0) {
132 +
133 +private function mxchat_fetch_conversation_history_for_ai($session_id) {
192 134 $history = get_option("mxchat_history_{$session_id}", []);
193 -
194 - // Check persistence setting - when OFF, only include messages from current page load
195 - $options = get_option('mxchat_options', []);
196 - $persistence_enabled = isset($options['chat_persistence_toggle']) && $options['chat_persistence_toggle'] === 'on';
197 -
198 - // Filter history when persistence is OFF to match what the user sees
199 - if (!$persistence_enabled && $session_start_timestamp > 0) {
200 - $history = array_filter($history, function($entry) use ($session_start_timestamp) {
201 - // Include messages from this page load onwards
202 - return isset($entry['timestamp']) && $entry['timestamp'] >= $session_start_timestamp;
203 - });
204 - // Re-index array after filtering
205 - $history = array_values($history);
206 - }
207 -
208 135 $formatted_history = [];
209 136
210 137 // Adjusted for code-heavy conversations
211 138 $max_tokens = 120000; // Context window size
@@ -303,15 +230,8 @@
303 230 'callback' => [$this, 'handle_slack_messages'],
304 231 'permission_callback' => [$this, 'verify_slack_request'],
305 232 ]);
306 233
307 - // Telegram webhook endpoint
308 - register_rest_route('mxchat/v1', '/telegram-webhook', [
309 - 'methods' => 'POST',
310 - 'callback' => [$this, 'handle_telegram_webhook'],
311 - 'permission_callback' => [$this, 'verify_telegram_request'],
312 - ]);
313 -
314 234 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
315 235 }
316 236
317 237 /**
@@ -351,11 +271,10 @@
351 271 //error_log(esc_html__('Slack request timestamp too old', 'mxchat'));
352 272 return false;
353 273 }
354 274
355 - // Get raw request body from the WP_REST_Request object
356 - // (php://input may already be consumed by WordPress at this point)
357 - $request_body = $request->get_body();
275 + // Get raw request body
276 + $request_body = file_get_contents('php://input');
358 277
359 278 // Create the signature base string
360 279 $sig_basestring = "v0:{$timestamp}:{$request_body}";
361 280
@@ -364,43 +283,8 @@
364 283
365 284 // Compare signatures
366 285 return hash_equals($my_signature, $slack_signature);
367 286 }
368 -
369 -/**
370 - * Verify request is coming from Telegram.
371 - *
372 - * @param WP_REST_Request $request
373 - * @return bool True if valid, false otherwise.
374 - */
375 -public function verify_telegram_request($request) {
376 - $secret_token = $this->options['telegram_webhook_secret'] ?? '';
377 -
378 - //error_log('[MxChat Telegram DEBUG] verify_telegram_request called');
379 - //error_log('[MxChat Telegram DEBUG] Stored secret: ' . (empty($secret_token) ? 'EMPTY' : substr($secret_token, 0, 10) . '...'));
380 -
381 - if (empty($secret_token)) {
382 - // If no secret is configured, allow the request (for initial setup)
383 - //error_log('[MxChat Telegram DEBUG] No secret configured, allowing request');
384 - return true;
385 - }
386 -
387 - // Telegram sends the secret token in the X-Telegram-Bot-Api-Secret-Token header
388 - $request_token = $request->get_header('X-Telegram-Bot-Api-Secret-Token');
389 -
390 - //error_log('[MxChat Telegram DEBUG] Request token: ' . (empty($request_token) ? 'EMPTY' : substr($request_token, 0, 10) . '...'));
391 -
392 - if (empty($request_token)) {
393 - //error_log('[MxChat Telegram DEBUG] Request rejected: No token in header');
394 - return false;
395 - }
396 -
397 - // Timing-safe comparison
398 - $result = hash_equals($secret_token, $request_token);
399 - //error_log('[MxChat Telegram DEBUG] Token comparison result: ' . ($result ? 'MATCH' : 'MISMATCH'));
400 - return $result;
401 -}
402 -
403 287 public function mxchat_stream_events(WP_REST_Request $request) {
404 288 header('Content-Type: text/event-stream');
405 289 header('Cache-Control: no-cache');
406 290 header('Connection: keep-alive');
@@ -434,9 +318,9 @@
434 318
435 319
436 320
437 321
438 -private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null, $rag_context = null) {
322 +private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null) {
439 323 global $wpdb;
440 324 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
441 325 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
442 326
@@ -454,20 +338,8 @@
454 338 //error_log("[DEBUG] This is a NEW session - first message");
455 339 }
456 340 }
457 341
458 - // SECURITY FIX: Set session ownership for new sessions
459 - if ($is_new_session && $role === 'user') {
460 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
461 - $session_owner_key = "mxchat_session_owner_{$session_id}";
462 -
463 - // Only set ownership if not already set
464 - if (!get_option($session_owner_key)) {
465 - update_option($session_owner_key, $current_user_identifier, 'no');
466 - //error_log("[DEBUG] Set session ownership for {$session_id} to {$current_user_identifier}");
467 - }
468 - }
469 -
470 342 // 1) Extract agent name if present
471 343 $agent_name = '';
472 344 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
473 345 $agent_name = $matches[1];
@@ -610,17 +482,9 @@
610 482 $insert_data['originating_page_title'] = $stored_originating['title'] ?? '';
611 483 }
612 484 }
613 485 }
614 -
615 - // Add RAG context if provided (for bot messages)
616 - if ($rag_context !== null && $role === 'bot') {
617 - $rag_context_column_exists = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'rag_context'");
618 - if ($rag_context_column_exists) {
619 - $insert_data['rag_context'] = is_array($rag_context) ? wp_json_encode($rag_context) : $rag_context;
620 - }
621 - }
622 -
486 +
623 487 $wpdb->insert($table_name, $insert_data);
624 488 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
625 489
626 490 // 9) Send notification email if this is the first user message in a new session
@@ -631,17 +495,11 @@
631 495 'ip' => $_SERVER['REMOTE_ADDR']
632 496 ));
633 497 }
634 498
635 - // 10) Schedule delayed transcript email if enabled and message is from user
636 - if ($wpdb->insert_id && $role === 'user') {
637 - $this->schedule_delayed_transcript_email($session_id);
638 - }
639 -
640 499 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
641 500 return $message_id;
642 501 }
643 -
644 502 private function send_new_chat_notification($session_id, $user_info = array()) {
645 503 $options = get_option('mxchat_transcripts_options');
646 504
647 505 // Check if notifications are enabled
@@ -684,200 +542,12 @@
684 542 // Send email
685 543 return wp_mail($to, $subject, $message);
686 544 }
687 545
688 -/**
689 - * Schedule delayed transcript email for a session
690 - * Reschedules if a new user message is received
691 - */
692 -private function schedule_delayed_transcript_email($session_id) {
693 - $options = get_option('mxchat_transcripts_options');
694 -
695 - // Check if auto-email is enabled
696 - if (empty($options['mxchat_auto_email_transcript_enabled'])) {
697 - return;
698 - }
699 -
700 - // Get notification email
701 - $email = !empty($options['mxchat_notification_email']) ?
702 - $options['mxchat_notification_email'] :
703 - get_option('admin_email');
704 -
705 - if (!is_email($email)) {
706 - return;
707 - }
708 -
709 - // Get delay in minutes (default 30)
710 - $delay_minutes = isset($options['mxchat_auto_email_transcript_delay']) ?
711 - intval($options['mxchat_auto_email_transcript_delay']) : 30;
712 -
713 - // Clear any existing scheduled event for this session
714 - $hook = 'mxchat_send_delayed_transcript';
715 - $args = array($session_id);
716 - $timestamp = wp_next_scheduled($hook, $args);
717 -
718 - if ($timestamp) {
719 - wp_unschedule_event($timestamp, $hook, $args);
720 - }
721 -
722 - // Schedule new event
723 - $schedule_time = time() + ($delay_minutes * 60);
724 - wp_schedule_single_event($schedule_time, $hook, $args);
725 -}
726 -
727 -/**
728 - * Check if chat messages contain contact information (email or phone number)
729 - *
730 - * @param array $messages Array of message objects with 'message' property
731 - * @param object|null $session_data Session data object with user_email property
732 - * @return bool True if contact info found, false otherwise
733 - */
734 -private function chat_contains_contact_info($messages, $session_data = null) {
735 - // Check if session already has a stored email
736 - if ($session_data && !empty($session_data->user_email)) {
737 - return true;
738 - }
739 -
740 - // Email regex pattern
741 - $email_pattern = '/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/';
742 -
743 - // Phone number patterns (covers various formats including international, WhatsApp style)
744 - // Matches: +1234567890, (123) 456-7890, 123-456-7890, 123.456.7890, 1234567890, +1 234 567 8900, etc.
745 - $phone_pattern = '/(?:\+?\d{1,3}[-.\s]?)?\(?\d{2,4}\)?[-.\s]?\d{2,4}[-.\s]?\d{2,4}(?:[-.\s]?\d{1,4})?/';
746 -
747 - // Only check user messages (not assistant responses)
748 - foreach ($messages as $msg) {
749 - if ($msg->role !== 'user') {
750 - continue;
751 - }
752 -
753 - $message_text = $msg->message;
754 -
755 - // Check for email
756 - if (preg_match($email_pattern, $message_text)) {
757 - return true;
758 - }
759 -
760 - // Check for phone number (must be at least 7 digits total to avoid false positives)
761 - if (preg_match($phone_pattern, $message_text, $matches)) {
762 - // Count actual digits to avoid matching short numbers
763 - $digits_only = preg_replace('/\D/', '', $matches[0]);
764 - if (strlen($digits_only) >= 7) {
765 - return true;
766 - }
767 - }
768 - }
769 -
770 - return false;
771 -}
772 -
773 -/**
774 - * Send the delayed transcript email with .txt attachment
775 - */
776 -public function mxchat_send_delayed_transcript($session_id) {
777 - global $wpdb;
778 -
779 - $options = get_option('mxchat_transcripts_options');
780 -
781 - // Get notification email
782 - $to = !empty($options['mxchat_notification_email']) ?
783 - $options['mxchat_notification_email'] :
784 - get_option('admin_email');
785 -
786 - if (!is_email($to)) {
787 - return false;
788 - }
789 -
790 - // Get all messages for this session
791 - $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
792 - $messages = $wpdb->get_results($wpdb->prepare(
793 - "SELECT role, message, timestamp FROM {$table_name}
794 - WHERE session_id = %s
795 - ORDER BY timestamp ASC",
796 - $session_id
797 - ));
798 -
799 - if (empty($messages)) {
800 - return false;
801 - }
802 -
803 - // Get session metadata
804 - $sessions_table = $wpdb->prefix . 'mxchat_sessions';
805 - $session_data = $wpdb->get_row($wpdb->prepare(
806 - "SELECT * FROM {$sessions_table} WHERE session_id = %s",
807 - $session_id
808 - ));
809 -
810 - // Check if contact info is required and if it's present
811 - $require_contact = !empty($options['mxchat_auto_email_transcript_require_contact']);
812 - if ($require_contact && !$this->chat_contains_contact_info($messages, $session_data)) {
813 - // Contact info required but not found - skip sending
814 - return false;
815 - }
816 -
817 - // Build transcript content
818 - $transcript_content = "Chat Transcript\n";
819 - $transcript_content .= "================\n\n";
820 - $transcript_content .= "Session ID: " . $session_id . "\n";
821 -
822 - if ($session_data) {
823 - $transcript_content .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
824 - $transcript_content .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
825 - $transcript_content .= "Started: " . $session_data->created_at . "\n";
826 - }
827 -
828 - $transcript_content .= "\n" . str_repeat("=", 50) . "\n\n";
829 -
830 - // Add messages
831 - foreach ($messages as $msg) {
832 - $role_label = ($msg->role === 'user') ? 'User' : 'Assistant';
833 - $transcript_content .= "[{$msg->timestamp}] {$role_label}:\n";
834 - $transcript_content .= $msg->message . "\n\n";
835 - }
836 -
837 - // Create temporary file for attachment using WP_Filesystem
838 - $upload_dir = wp_upload_dir();
839 - $temp_file = $upload_dir['basedir'] . '/mxchat-transcript-' . $session_id . '.txt';
840 - global $wp_filesystem;
841 - if (empty($wp_filesystem)) {
842 - require_once ABSPATH . 'wp-admin/includes/file.php';
843 - WP_Filesystem();
844 - }
845 - $wp_filesystem->put_contents($temp_file, $transcript_content, FS_CHMOD_FILE);
846 -
847 - // Prepare email
848 - $subject = sprintf('[%s] Chat Transcript - Session %s', get_bloginfo('name'), substr($session_id, 0, 8));
849 -
850 - $message = "Please find attached the full chat transcript.\n\n";
851 - $message .= "Session ID: {$session_id}\n";
852 -
853 - if ($session_data) {
854 - $message .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
855 - $message .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
856 - }
857 -
858 - $message .= "\nView online: " . admin_url('admin.php?page=mxchat-transcripts');
859 -
860 - // Send email with attachment
861 - $attachments = array($temp_file);
862 - $result = wp_mail($to, $subject, $message, '', $attachments);
863 -
864 - // Clean up temporary file
865 - if (file_exists($temp_file)) {
866 - unlink($temp_file);
867 - }
868 -
869 - return $result;
870 -}
871 -
872 -
873 -
874 546 public function mxchat_handle_save_email_and_response() {
875 547 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
876 548 //error_log('DEBUG: POST data: ' . print_r($_POST, true));
877 549
878 - nocache_headers();
879 -
880 550 // Validate nonce
881 551 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
882 552 //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
883 553 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
@@ -889,9 +559,9 @@
889 559 $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
890 560
891 561 //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}, name: {$name}");
892 562
893 - if (empty($session_id) || $session_id === 'null' || empty($email)) {
563 + if (empty($session_id) || empty($email)) {
894 564 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
895 565 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
896 566 wp_die();
897 567 }
@@ -908,15 +578,15 @@
908 578 }
909 579
910 580 // 1) Always store email in wp_options
911 581 $email_option_key = "mxchat_email_{$session_id}";
912 - update_option($email_option_key, $email, 'no');
582 + update_option($email_option_key, $email);
913 583 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$email_option_key} => {$email}");
914 584
915 585 // Store name in wp_options if provided
916 586 if (!empty($name)) {
917 587 $name_option_key = "mxchat_name_{$session_id}";
918 - update_option($name_option_key, $name, 'no');
588 + update_option($name_option_key, $name);
919 589 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$name_option_key} => {$name}");
920 590 }
921 591
922 592 // 2) (Optional) Also store in DB if a row already exists
@@ -960,10 +630,8 @@
960 630
961 631 public function mxchat_check_email_provided() {
962 632 //error_log('[DEBUG] ---------- mxchat_check_email_provided START ----------');
963 633
964 - nocache_headers();
965 -
966 634 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
967 635 //error_log('[ERROR] Invalid nonce in mxchat_check_email_provided');
968 636 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
969 637 }
@@ -968,9 +636,9 @@
968 636 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
969 637 }
970 638
971 639 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
972 - if (empty($session_id) || $session_id === 'null') {
640 + if (empty($session_id)) {
973 641 //error_log('[ERROR] No session ID provided in mxchat_check_email_provided');
974 642 wp_send_json_error(['message' => esc_html__('No session ID provided', 'mxchat')]);
975 643 }
976 644
@@ -1027,42 +695,15 @@
1027 695 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
1028 696 }
1029 697 }
1030 698
1031 -/**
1032 - * Send error response in appropriate format based on streaming mode
1033 - * ADDED: Helper method to consistently handle errors in both streaming and non-streaming modes
1034 - *
1035 - * @param string $error_message The error message to display
1036 - * @param string $error_code Optional error code for debugging
1037 - */
1038 -private function send_error_response($error_message, $error_code = 'api_error') {
1039 - if ($this->is_streaming) {
1040 - echo "data: " . json_encode([
1041 - 'error' => true,
1042 - 'error_message' => $error_message,
1043 - 'error_code' => $error_code,
1044 - 'text' => $error_message,
1045 - 'message' => $error_message
1046 - ]) . "\n\n";
1047 - echo "data: [DONE]\n\n";
1048 - flush();
1049 - } else {
1050 - wp_send_json_error([
1051 - 'error_message' => $error_message,
1052 - 'error_code' => $error_code
1053 - ]);
1054 - }
1055 - wp_die();
1056 -}
1057 -
1058 699 public function mxchat_handle_chat_request() {
1059 700 global $wpdb;
1060 701
1061 702 // Debug: Log incoming bot_id
1062 703 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
1063 - //error_log("=== MXCHAT DEBUG: Starting chat request ===");
1064 - //error_log("MXCHAT DEBUG: Bot ID received: " . $bot_id);
704 + error_log("=== MXCHAT DEBUG: Starting chat request ===");
705 + error_log("MXCHAT DEBUG: Bot ID received: " . $bot_id);
1065 706
1066 707 // Get bot-specific options
1067 708 $bot_options = $this->get_bot_options($bot_id);
1068 709 $current_options = !empty($bot_options) ? $bot_options : $this->options;
@@ -1067,19 +708,29 @@
1067 708 $bot_options = $this->get_bot_options($bot_id);
1068 709 $current_options = !empty($bot_options) ? $bot_options : $this->options;
1069 710
1070 711 // Check if this is a streaming request
1071 - // Allow force_streaming_test parameter to bypass the setting check (for admin compatibility testing)
1072 - $force_streaming_test = isset($_POST['force_streaming_test']) && $_POST['force_streaming_test'] === '1' && current_user_can('administrator');
1073 - $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat' &&
1074 - ($force_streaming_test || (isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on'));
712 + $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat' &&
713 + isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on';
714 +
715 + // Set streaming headers if needed
716 + if ($is_streaming) {
717 + // Disable output buffering
718 + while (ob_get_level()) {
719 + ob_end_flush(); // Changed from ob_end_clean()
720 + }
721 +
722 + // Set headers for SSE
723 + header('Content-Type: text/event-stream');
724 + header('Cache-Control: no-cache');
725 + header('Connection: keep-alive');
726 + header('X-Accel-Buffering: no');
727 +
728 + // Add these new lines:
729 + ob_implicit_flush(true);
730 + flush();
731 + }
1075 732
1076 - // ADDED: Store streaming state in class property for use in private methods
1077 - $this->is_streaming = $is_streaming;
1078 -
1079 - // NOTE: Streaming headers are now set later via setup_streaming_headers()
1080 - // This allows actions/forms to return JSON responses without header conflicts
1081 -
1082 733 // Check if MX Chat Moderation is active
1083 734 if (class_exists('MX_Chat_Moderation')) {
1084 735 // Get user email and IP
1085 736 $user_email = '';
@@ -1144,31 +795,13 @@
1144 795
1145 796 // Rest of your existing code...
1146 797 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
1147 798
1148 - // Treat the literal strings 'null' / 'undefined' as missing too. Browser edge cases
1149 - // (Safari ITP, private mode, cross-origin iframes with partitioned storage) can cause
1150 - // the frontend FormData.append() to stringify a null session_id into the literal
1151 - // "null", which would otherwise pass empty() and pollute the transcripts table with
1152 - // ghost sessions that group every visitor's first message under one row.
1153 - if ($session_id === 'null' || $session_id === 'undefined') {
1154 - $session_id = '';
1155 - }
1156 -
1157 799 if (empty($session_id)) {
1158 800 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
1159 801 wp_die();
1160 802 }
1161 803
1162 - // Update session owner if it changed (e.g. IP changed due to network switch)
1163 - // The session ID itself is the authentication — if the client has it, they own it
1164 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
1165 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
1166 -
1167 - if (!$session_owner || $session_owner !== $current_user_identifier) {
1168 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
1169 - }
1170 -
1171 804 // Validate and sanitize the incoming message
1172 805 if (empty($_POST['message'])) {
1173 806 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
1174 807 wp_die();
@@ -1544,8 +1177,38 @@
1544 1177 }
1545 1178 }
1546 1179 }
1547 1180
1181 + // Check if there's an active recommendation flow session
1182 + $flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
1183 + if (!empty($flow_state) && isset($flow_state['flow_id'])) {
1184 + // Create a dummy intent object that matches the original intent
1185 + $dummy_intent = new stdClass();
1186 + $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
1187 + $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
1188 +
1189 + // Call the recommendation flow handler directly
1190 + $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
1191 +
1192 + // If the handler returned a response, send it
1193 + if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
1194 + // Save the bot's response to the chat history
1195 + if (!empty($response_data['text'])) {
1196 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
1197 + }
1198 + if (!empty($response_data['html'])) {
1199 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
1200 + }
1201 +
1202 + if ($testing_data !== null) {
1203 + $response_data['testing_data'] = $testing_data;
1204 + }
1205 +
1206 + // Send the response
1207 + wp_send_json($response_data);
1208 + wp_die();
1209 + }
1210 + }
1548 1211
1549 1212 // Step 2: Detect intent and handle intent-based responses
1550 1213 $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1551 1214
@@ -1564,46 +1227,58 @@
1564 1227 'text' => $intent_result['text'] ?? '',
1565 1228 'html' => $intent_result['html'] ?? '',
1566 1229 'session_id' => $session_id
1567 1230 ];
1568 -
1569 - // IMPORTANT: Include chat_mode if present (for WhatsApp, Slack, etc.)
1570 - if (isset($intent_result['chat_mode'])) {
1571 - $response_data['chat_mode'] = $intent_result['chat_mode'];
1572 - }
1573 -
1231 +
1574 1232 if ($testing_data !== null) {
1575 1233 $response_data['testing_data'] = $testing_data;
1576 1234 }
1577 -
1235 +
1236 + // Clear streaming headers if they were set
1237 + if ($is_streaming) {
1238 + header_remove('Content-Type');
1239 + header_remove('Cache-Control');
1240 + header_remove('Connection');
1241 + header_remove('X-Accel-Buffering');
1242 + header('Content-Type: application/json');
1243 + }
1244 +
1578 1245 wp_send_json($response_data);
1579 1246 wp_die();
1580 1247 } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1581 1248 // Intent returned true and set fallbackResponse
1582 -
1583 - // SAVE TO TRANSCRIPT
1249 +
1250 + // SAVE TO TRANSCRIPT FIRST
1584 1251 if (!empty($this->fallbackResponse['text'])) {
1585 1252 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1586 1253 }
1587 - // Save action HTML (product cards, featured products, etc.) so it renders in transcripts
1588 1254 if (!empty($this->fallbackResponse['html'])) {
1589 1255 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1590 1256 }
1591 -
1257 +
1592 1258 $response_data = [
1593 1259 'text' => $this->fallbackResponse['text'] ?? '',
1594 1260 'html' => $this->fallbackResponse['html'] ?? '',
1595 1261 'session_id' => $session_id
1596 1262 ];
1597 -
1263 +
1598 1264 if (isset($this->fallbackResponse['chat_mode'])) {
1599 1265 $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
1600 1266 }
1601 -
1267 +
1602 1268 if ($testing_data !== null) {
1603 1269 $response_data['testing_data'] = $testing_data;
1604 1270 }
1605 -
1271 +
1272 + // Clear streaming headers if they were set
1273 + if ($is_streaming) {
1274 + header_remove('Content-Type');
1275 + header_remove('Cache-Control');
1276 + header_remove('Connection');
1277 + header_remove('X-Accel-Buffering');
1278 + header('Content-Type: application/json');
1279 + }
1280 +
1606 1281 wp_send_json($response_data);
1607 1282 wp_die();
1608 1283 }
1609 1284 }
@@ -1608,13 +1283,11 @@
1608 1283 }
1609 1284 }
1610 1285
1611 1286 // If we get here, no intent matched OR the intent didn't provide a usable response
1612 -
1287 +
1613 1288 // Step 4: Generate AI response
1614 - // Get session start timestamp - when persistence is OFF, only include messages from this page load
1615 - $session_start_timestamp = isset($_POST['session_start_timestamp']) ? intval($_POST['session_start_timestamp']) : 0;
1616 - $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id, $session_start_timestamp);
1289 + $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
1617 1290 $this->mxchat_increment_chat_count();
1618 1291
1619 1292 // Generate embedding for the user's query - USE BOT-SPECIFIC API KEY
1620 1293 $api_key = $current_options['api_key'] ?? $this->options['api_key'];
@@ -1623,50 +1296,22 @@
1623 1296 // Check if the embedding generation returned an error
1624 1297 if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1625 1298 $error_message = $user_message_embedding['error'];
1626 1299 $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1627 -
1628 - // FIXED: Send error in appropriate format based on streaming mode
1629 - if ($is_streaming) {
1630 - echo "data: " . json_encode([
1631 - 'error' => true,
1632 - 'error_message' => $error_message,
1633 - 'error_code' => $error_code,
1634 - 'text' => $error_message,
1635 - 'message' => $error_message
1636 - ]) . "\n\n";
1637 - echo "data: [DONE]\n\n";
1638 - flush();
1639 - } else {
1640 - wp_send_json_error([
1641 - 'error_message' => $error_message,
1642 - 'error_code' => $error_code
1643 - ]);
1644 - }
1300 +
1301 + wp_send_json_error([
1302 + 'error_message' => $error_message,
1303 + 'error_code' => $error_code
1304 + ]);
1645 1305 wp_die();
1646 1306 }
1647 -
1307 +
1648 1308 // Check if the embedding is valid
1649 1309 if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1650 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
1651 -
1652 - // FIXED: Send error in appropriate format based on streaming mode
1653 - if ($is_streaming) {
1654 - echo "data: " . json_encode([
1655 - 'error' => true,
1656 - 'error_message' => $error_message,
1657 - 'error_code' => 'invalid_embedding',
1658 - 'text' => $error_message,
1659 - 'message' => $error_message
1660 - ]) . "\n\n";
1661 - echo "data: [DONE]\n\n";
1662 - flush();
1663 - } else {
1664 - wp_send_json_error([
1665 - 'error_message' => $error_message,
1666 - 'error_code' => 'invalid_embedding'
1667 - ]);
1668 - }
1310 + wp_send_json_error([
1311 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1312 + 'error_code' => 'invalid_embedding'
1313 + ]);
1669 1314 wp_die();
1670 1315 }
1671 1316
1672 1317 // Build context with both knowledge base and PDF content if available
@@ -1692,71 +1337,26 @@
1692 1337 $context_content .= "Page Content: " . $page_context['content'] . "\n";
1693 1338 $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1694 1339 }
1695 1340
1696 - // Get relevant content from knowledge base - PASS BOT_ID and MESSAGE for Vector Store
1697 - $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id, $message);
1698 -
1699 - // NEW: Also extract URLs from system instructions (only if citation links enabled)
1700 - // Use fresh options to ensure we get the latest setting value
1701 - $fresh_options = get_option('mxchat_options', []);
1702 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
1341 + // Get relevant content from knowledge base - PASS BOT_ID
1342 + $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id);
1703 1343
1704 - $system_instructions = $this->get_system_instructions($bot_id, $session_id);
1705 - if ($citation_links_enabled && !empty($system_instructions)) {
1706 - preg_match_all(
1707 - '#\bhttps?://[^\s<>"\']+#i',
1708 - $system_instructions,
1709 - $system_instruction_urls
1710 - );
1711 -
1712 - if (!empty($system_instruction_urls[0])) {
1713 - // Merge with existing valid URLs
1714 - $this->current_valid_urls = array_merge(
1715 - $this->current_valid_urls,
1716 - $system_instruction_urls[0]
1717 - );
1718 - // Remove duplicates
1719 - $this->current_valid_urls = array_unique($this->current_valid_urls);
1720 -
1721 - //error_log("Added " . count($system_instruction_urls[0]) . " URLs from system instructions");
1722 - }
1344 + // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1345 + if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1346 + // Update testing data with the REAL similarity analysis
1347 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1348 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1349 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1723 1350 }
1351 + // ===== END SIMILARITY DATA CAPTURE =====
1724 1352
1725 -// ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1726 -if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1727 - // Update testing data with the REAL similarity analysis
1728 - $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1729 - $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1730 - $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1731 - $testing_data['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
1732 - $testing_data['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
1733 -}
1734 -// ===== END SIMILARITY DATA CAPTURE =====
1735 -
1736 -// NEW: Add valid URLs to testing data for admin panel display (AFTER similarity data)
1737 -if ($testing_data !== null && !empty($this->current_valid_urls)) {
1738 - $testing_data['approved_urls'] = array_values($this->current_valid_urls);
1739 - //error_log("Added " . count($this->current_valid_urls) . " approved URLs to testing data");
1740 -}
1741 -
1742 1353 if (!empty($relevant_content)) {
1743 1354 $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1744 1355 } else {
1745 1356 $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1746 1357 }
1747 -
1748 - // NEW: Add approved URLs list to context for AI (only if citation links enabled)
1749 - if ($citation_links_enabled && !empty($this->current_valid_urls)) {
1750 - $context_content .= "===== APPROVED URLS FOR CITATIONS =====\n";
1751 - $context_content .= "You may ONLY use these exact URLs in your response:\n";
1752 - foreach ($this->current_valid_urls as $url) {
1753 - $context_content .= "- " . $url . "\n";
1754 - }
1755 - $context_content .= "\nCRITICAL: Do NOT create, modify, extend, or invent any other URLs. ";
1756 - $context_content .= "===== END APPROVED URLS =====\n\n";
1757 - }
1758 -
1358 +
1759 1359 // Check for and include PDF content
1760 1360 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1761 1361 $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1762 1362 $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
@@ -1788,9 +1388,9 @@
1788 1388
1789 1389 $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1790 1390
1791 1391 // Extract model from current options for bot-specific model support
1792 - $selected_model = isset($current_options['model']) ? $current_options['model'] : 'gpt-5.1-chat-latest';
1392 + $selected_model = isset($current_options['model']) ? $current_options['model'] : 'gpt-4o';
1793 1393
1794 1394 $response = $this->mxchat_generate_response(
1795 1395 $context_content,
1796 1396 $current_options['api_key'] ?? $this->options['api_key'],
@@ -1797,14 +1397,13 @@
1797 1397 $current_options['xai_api_key'] ?? $this->options['xai_api_key'],
1798 1398 $current_options['claude_api_key'] ?? $this->options['claude_api_key'],
1799 1399 $current_options['deepseek_api_key'] ?? $this->options['deepseek_api_key'],
1800 1400 $current_options['gemini_api_key'] ?? $this->options['gemini_api_key'],
1801 - $current_options['openrouter_api_key'] ?? $this->options['openrouter_api_key'],
1802 1401 $conversation_history,
1803 1402 $is_streaming,
1804 1403 $session_id,
1805 1404 $testing_data,
1806 - $selected_model
1405 + $selected_model // ADD THIS LINE
1807 1406 );
1808 1407
1809 1408 // Handle streaming vs non-streaming responses
1810 1409 if ($is_streaming) {
@@ -1812,27 +1411,11 @@
1812 1411 if ($response === true) {
1813 1412 wp_die();
1814 1413 }
1815 1414 // If we get here, streaming fell back to regular response, continue
1816 - // But if there's an error, we need to send it as SSE format since headers are already set
1817 - if (is_array($response) && isset($response['error'])) {
1818 - $error_message = $response['error'];
1819 - $error_code = $response['error_code'] ?? 'api_error';
1820 - // Send error in SSE format that the client JS can handle
1821 - echo "data: " . json_encode([
1822 - 'error' => true,
1823 - 'error_message' => $error_message,
1824 - 'error_code' => $error_code,
1825 - 'text' => $error_message, // Also include as text for fallback handling
1826 - 'message' => $error_message
1827 - ]) . "\n\n";
1828 - echo "data: [DONE]\n\n";
1829 - flush();
1830 - wp_die();
1831 - }
1832 1415 }
1833 -
1834 - // Check if the response is an error array (non-streaming mode)
1416 +
1417 + // Check if the response is an error array
1835 1418 if (is_array($response) && isset($response['error'])) {
1836 1419 wp_send_json_error([
1837 1420 'error_message' => $response['error'],
1838 1421 'error_code' => $response['error_code'] ?? 'api_error'
@@ -1839,51 +1422,11 @@
1839 1422 ]);
1840 1423 wp_die();
1841 1424 }
1842 1425
1843 - // DEBUG: Check what we have
1844 - //error_log("=== BEFORE URL VALIDATION ===");
1845 - //error_log("current_valid_urls is empty? " . (empty($this->current_valid_urls) ? 'YES' : 'NO'));
1846 - //error_log("current_valid_urls count: " . count($this->current_valid_urls));
1847 - //error_log("current_valid_urls content: " . print_r($this->current_valid_urls, true));
1848 -
1849 - // If we get here, the response is valid text - now validate URLs
1850 - if (!empty($this->current_valid_urls)) {
1851 - //error_log("CALLING validate_and_clean_urls");
1852 - $response = $this->validate_and_clean_urls($response, $this->current_valid_urls);
1853 - } else {
1854 - //error_log("SKIPPING validation - current_valid_urls is empty");
1855 - }
1856 - // ===== END URL VALIDATION =====
1426 + // If we get here, the response is valid text
1427 + $this->mxchat_save_chat_message($session_id, 'bot', $response);
1857 1428
1858 - // Prepare RAG context data for storage (only include documents used for context)
1859 - $rag_context_for_storage = null;
1860 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
1861 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
1862 -
1863 - if ($has_rag_data || $has_action_data) {
1864 - $rag_context_for_storage = [];
1865 -
1866 - // Add RAG/source data if available
1867 - if ($has_rag_data) {
1868 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
1869 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
1870 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
1871 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
1872 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1873 - $rag_context_for_storage['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
1874 - $rag_context_for_storage['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
1875 - }
1876 -
1877 - // Add action analysis data if available
1878 - if ($has_action_data) {
1879 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
1880 - }
1881 - }
1882 -
1883 - // Save the cleaned response with RAG context
1884 - $this->mxchat_save_chat_message($session_id, 'bot', $response, null, $rag_context_for_storage);
1885 -
1886 1429 // Step 5: Save additional content if available
1887 1430 if (!empty($this->productCardHtml)) {
1888 1431 $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1889 1432 }
@@ -1892,13 +1435,8 @@
1892 1435 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1893 1436 }
1894 1437
1895 1438 // Step 6: Return the response
1896 - // DEBUG: Check if newlines exist in the response
1897 - //error_log("=== MXCHAT NON-STREAMING RESPONSE DEBUG ===");
1898 - //error_log("Response has newlines: " . (strpos($response, "\n") !== false ? 'YES' : 'NO'));
1899 - //error_log("Response first 500 chars: " . substr($response, 0, 500));
1900 -
1901 1439 $response_data = [
1902 1440 'text' => $response,
1903 1441 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1904 1442 'session_id' => $session_id
@@ -1903,18 +1441,8 @@
1903 1441 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1904 1442 'session_id' => $session_id
1905 1443 ];
1906 1444
1907 - // Include vectorstore error info for admin debugging (only visible to admins via testing_data)
1908 - if (!empty($this->last_vectorstore_error) && $testing_data !== null) {
1909 - $testing_data['vectorstore_error'] = $this->last_vectorstore_error;
1910 - }
1911 -
1912 - // Also pass it as a top-level field so JS can show a better error message to admins
1913 - if (!empty($this->last_vectorstore_error) && current_user_can('manage_options')) {
1914 - $response_data['vectorstore_error'] = $this->last_vectorstore_error;
1915 - }
1916 -
1917 1445 // Always add testing data for admins (no toggle needed)
1918 1446 if ($testing_data !== null) {
1919 1447 $response_data['testing_data'] = $testing_data;
1920 1448 }
@@ -1922,8 +1450,9 @@
1922 1450 wp_send_json($response_data);
1923 1451 wp_die();
1924 1452 }
1925 1453
1454 +
1926 1455 /**
1927 1456 * Get bot-specific options for multi-bot functionality
1928 1457 * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1929 1458 */
@@ -1928,12 +1457,12 @@
1928 1457 * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1929 1458 */
1930 1459 // Also debug the bot options retrieval
1931 1460 private function get_bot_options($bot_id = 'default') {
1932 - //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
1461 + error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
1933 1462
1934 1463 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1935 - //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
1464 + error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
1936 1465 return array();
1937 1466 }
1938 1467
1939 1468 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
@@ -1938,11 +1467,11 @@
1938 1467
1939 1468 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
1940 1469
1941 1470 if (!empty($bot_options)) {
1942 - //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1471 + error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1943 1472 if (isset($bot_options['similarity_threshold'])) {
1944 - //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1473 + error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1945 1474 }
1946 1475 }
1947 1476
1948 1477 return is_array($bot_options) ? $bot_options : array();
@@ -1953,13 +1482,13 @@
1953 1482 * Used in the knowledge retrieval functions
1954 1483 */
1955 1484 // Also add debugging to your get_bot_pinecone_config function
1956 1485 private function get_bot_pinecone_config($bot_id = 'default') {
1957 - //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1486 + error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1958 1487
1959 1488 // If default bot or multi-bot add-on not active, use default Pinecone config
1960 1489 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1961 - //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1490 + error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1962 1491 $addon_options = get_option('mxchat_pinecone_addon_options', array());
1963 1492 $config = array(
1964 1493 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
1965 1494 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
@@ -1965,24 +1494,24 @@
1965 1494 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
1966 1495 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
1967 1496 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
1968 1497 );
1969 - //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1498 + error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1970 1499 return $config;
1971 1500 }
1972 1501
1973 - //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1502 + error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1974 1503
1975 1504 // Hook for multi-bot add-on to provide bot-specific Pinecone config
1976 1505 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1977 1506
1978 1507 if (!empty($bot_pinecone_config)) {
1979 - //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1980 - //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1981 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1982 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1508 + error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1509 + error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1510 + error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1511 + error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1983 1512 } else {
1984 - //error_log("MXCHAT DEBUG: Filter returned empty config!");
1513 + error_log("MXCHAT DEBUG: Filter returned empty config!");
1985 1514 }
1986 1515
1987 1516 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1988 1517 }
@@ -1992,63 +1521,35 @@
1992 1521 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
1993 1522 global $wpdb;
1994 1523 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1995 1524
1996 - // Get the current bot_id
1525 + // NEW: Get the current bot_id
1997 1526 $current_bot_id = $this->get_current_bot_id($session_id);
1998 1527
1999 1528 // Generate the user embedding
2000 1529 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
2001 -
1530 +
2002 1531 // Check if embedding generation returned an error
2003 1532 if (is_array($user_embedding) && isset($user_embedding['error'])) {
2004 1533 $error_message = $user_embedding['error'];
2005 1534 $error_code = $user_embedding['error_code'] ?? 'embedding_error';
2006 -
2007 - // FIXED: Send error in appropriate format based on streaming mode
2008 - if ($this->is_streaming) {
2009 - echo "data: " . json_encode([
2010 - 'error' => true,
2011 - 'error_message' => $error_message,
2012 - 'error_code' => $error_code,
2013 - 'text' => $error_message,
2014 - 'message' => $error_message
2015 - ]) . "\n\n";
2016 - echo "data: [DONE]\n\n";
2017 - flush();
2018 - } else {
2019 - wp_send_json_error([
2020 - 'error_message' => $error_message,
2021 - 'error_code' => $error_code
2022 - ]);
2023 - }
1535 +
1536 + wp_send_json_error([
1537 + 'error_message' => $error_message,
1538 + 'error_code' => $error_code
1539 + ]);
2024 1540 wp_die();
2025 1541 }
2026 -
1542 +
2027 1543 // Check if embedding is valid
2028 1544 if (!is_array($user_embedding) || empty($user_embedding)) {
2029 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
2030 -
2031 - // FIXED: Send error in appropriate format based on streaming mode
2032 - if ($this->is_streaming) {
2033 - echo "data: " . json_encode([
2034 - 'error' => true,
2035 - 'error_message' => $error_message,
2036 - 'error_code' => 'invalid_embedding',
2037 - 'text' => $error_message,
2038 - 'message' => $error_message
2039 - ]) . "\n\n";
2040 - echo "data: [DONE]\n\n";
2041 - flush();
2042 - } else {
2043 - wp_send_json_error([
2044 - 'error_message' => $error_message,
2045 - 'error_code' => 'invalid_embedding'
2046 - ]);
2047 - }
1545 + wp_send_json_error([
1546 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1547 + 'error_code' => 'invalid_embedding'
1548 + ]);
2048 1549 wp_die();
2049 1550 }
2050 -
1551 +
2051 1552 // Fetch intents from the database
2052 1553 $table_name = $wpdb->prefix . 'mxchat_intents';
2053 1554 if ($chat_mode === 'agent') {
2054 1555 $query = $wpdb->prepare(
@@ -2058,29 +1559,19 @@
2058 1559 $intents = $wpdb->get_results($query);
2059 1560 } else {
2060 1561 $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
2061 1562 }
2062 -
1563 +
2063 1564 if (empty($intents)) {
2064 1565 return false;
2065 1566 }
2066 -
2067 - // Prefetch individual phrase vectors from wp_mxchat_intent_phrases (grouped by intent_id)
2068 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
2069 - $phrases_by_intent = [];
2070 - if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
2071 - $all_phrases = $wpdb->get_results("SELECT intent_id, phrase, embedding_vector FROM $phrases_table");
2072 - foreach ($all_phrases as $p) {
2073 - $phrases_by_intent[$p->intent_id][] = $p;
2074 - }
2075 - }
2076 -
1567 +
2077 1568 $highest_similarity = -INF;
2078 1569 $matched_intent = null;
2079 -
1570 +
2080 1571 // Array to store action analysis for testing panel
2081 1572 $action_analysis = [];
2082 -
1573 +
2083 1574 foreach ($intents as $intent) {
2084 1575 // Additional check for enabled state
2085 1576 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2086 1577 if (!$is_enabled) {
@@ -2085,56 +1576,26 @@
2085 1576 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2086 1577 if (!$is_enabled) {
2087 1578 continue;
2088 1579 }
2089 -
2090 - // Check if this action is enabled for the current bot
1580 +
1581 + // NEW: Check if this action is enabled for the current bot
2091 1582 if (!$this->is_action_enabled_for_bot($intent, $current_bot_id)) {
2092 1583 continue;
2093 1584 }
2094 -
2095 - $best_similarity = -INF;
2096 - $matched_phrase_text = '';
2097 -
2098 - // Check legacy embedding vector (existing behavior)
1585 +
2099 1586 $intent_embedding_serialized = $intent->embedding_vector;
2100 1587 $intent_embedding = $intent_embedding_serialized
2101 1588 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
2102 1589 : null;
2103 -
2104 - if (is_array($intent_embedding) && !empty($intent_embedding)) {
2105 - $legacy_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2106 - if ($legacy_similarity > $best_similarity) {
2107 - $best_similarity = $legacy_similarity;
2108 - $matched_phrase_text = 'legacy';
2109 - }
2110 - }
2111 -
2112 - // Check individual phrase vectors
2113 - if (isset($phrases_by_intent[$intent->id])) {
2114 - foreach ($phrases_by_intent[$intent->id] as $phrase_row) {
2115 - $phrase_embedding = $phrase_row->embedding_vector
2116 - ? unserialize($phrase_row->embedding_vector, ['allowed_classes' => false])
2117 - : null;
2118 - if (!is_array($phrase_embedding)) {
2119 - continue;
2120 - }
2121 - $phrase_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $phrase_embedding);
2122 - if ($phrase_similarity > $best_similarity) {
2123 - $best_similarity = $phrase_similarity;
2124 - $matched_phrase_text = $phrase_row->phrase;
2125 - }
2126 - }
2127 - }
2128 -
2129 - // Skip if no valid embedding was found at all
2130 - if ($best_similarity === -INF) {
1590 +
1591 + if (!is_array($intent_embedding)) {
2131 1592 continue;
2132 1593 }
2133 -
2134 - $similarity = $best_similarity;
1594 +
1595 + $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2135 1596 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
2136 -
1597 +
2137 1598 // Store action analysis data for testing panel
2138 1599 $action_analysis[] = [
2139 1600 'intent_label' => $intent->intent_label,
2140 1601 'callback_function' => $intent->callback_function,
@@ -2142,12 +1603,11 @@
2142 1603 'similarity_percentage' => round($similarity * 100, 2),
2143 1604 'threshold' => $intent_threshold,
2144 1605 'threshold_percentage' => round($intent_threshold * 100, 2),
2145 1606 'above_threshold' => $similarity >= $intent_threshold,
2146 - 'matched_phrase' => $matched_phrase_text,
2147 1607 'triggered' => false // Will be updated below if this intent is triggered
2148 1608 ];
2149 -
1609 +
2150 1610 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
2151 1611 $highest_similarity = $similarity;
2152 1612 $matched_intent = $intent;
2153 1613 }
@@ -2218,22 +1678,16 @@
2218 1678 // If enabled_bots column doesn't exist or is null, assume it's enabled for all bots (backward compatibility)
2219 1679 if (!isset($intent->enabled_bots) || empty($intent->enabled_bots)) {
2220 1680 return true;
2221 1681 }
2222 -
1682 +
2223 1683 $enabled_bots = json_decode($intent->enabled_bots, true);
2224 -
1684 +
2225 1685 // If JSON decode fails or returns empty array, assume enabled for all (backward compatibility)
2226 1686 if (!is_array($enabled_bots) || empty($enabled_bots)) {
2227 1687 return true;
2228 1688 }
2229 -
2230 - // Admin testing tab uses bot_id "testing" — treat it as "default" so all
2231 - // default-bot actions are testable from the admin panel
2232 - if ($bot_id === 'testing') {
2233 - $bot_id = 'default';
2234 - }
2235 -
1689 +
2236 1690 // Check if the current bot is in the enabled bots list
2237 1691 return in_array($bot_id, $enabled_bots);
2238 1692 }
2239 1693
@@ -2272,17 +1726,17 @@
2272 1726
2273 1727 public function mxchat_generate_image($message, $user_id, $session_id) {
2274 1728 //error_log("Starting image generation for message: " . $message);
2275 1729
2276 - // Prepare a prompt for OpenAI image generation
1730 + // Prepare a prompt for DALL-E
2277 1731 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2278 -
1732 +
2279 1733 // Use the existing OpenAI API key
2280 1734 $openai_api_key = sanitize_text_field($this->options['api_key']);
2281 -
2282 - // Call OpenAI GPT Image to generate an image
2283 - $image_response = $this->mxchat_generate_openai_image($prompt, $openai_api_key);
2284 1735
1736 + // Call DALL-E to generate an image
1737 + $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1738 +
2285 1739 // Check if the response contains an image URL
2286 1740 if (isset($image_response['imageUrl'])) {
2287 1741 $image_url = esc_url_raw($image_response['imageUrl']);
2288 1742
@@ -2325,103 +1779,24 @@
2325 1779 // Return the response directly instead of relying on the property
2326 1780 return $this->fallbackResponse;
2327 1781 }
2328 1782 }
2329 -
2330 -public function mxchat_generate_gemini_image($message, $user_id, $session_id) {
2331 - $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2332 -
2333 - $gemini_api_key = sanitize_text_field($this->options['gemini_api_key'] ?? '');
2334 - if (empty($gemini_api_key)) {
2335 - $response_text = esc_html__("Gemini API key is not configured.", 'mxchat');
2336 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2337 - return ['text' => $response_text, 'html' => '', 'images' => []];
2338 - }
2339 -
2340 - $image_response = $this->mxchat_generate_imagen_image($prompt, $gemini_api_key);
2341 -
2342 - if (isset($image_response['imageUrl'])) {
2343 - $image_url = esc_url_raw($image_response['imageUrl']);
2344 -
2345 - $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
2346 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
2347 -
2348 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2349 - $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
2350 -
2351 - $this->fallbackResponse = [
2352 - 'text' => $response_text,
2353 - 'html' => $response_html,
2354 - 'images' => [$image_url]
2355 - ];
2356 -
2357 - return $this->fallbackResponse;
2358 - } else {
2359 - $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
2360 -
2361 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2362 -
2363 - $this->fallbackResponse = [
2364 - 'text' => $response_text,
2365 - 'html' => '',
2366 - 'images' => []
2367 - ];
2368 -
2369 - return $this->fallbackResponse;
2370 - }
2371 -}
2372 -
2373 -private function mxchat_save_generated_image($base64_data, $mime_type = 'image/png', $prefix = 'mxchat-generated') {
2374 - $extension = ($mime_type === 'image/jpeg') ? 'jpg' : 'png';
2375 - $filename = sanitize_file_name($prefix . '-' . wp_generate_uuid4() . '.' . $extension);
2376 - $decoded = base64_decode($base64_data);
2377 -
2378 - if ($decoded === false) {
2379 - return new \WP_Error('decode_failed', esc_html__('Failed to decode image data.', 'mxchat'));
2380 - }
2381 -
2382 - $upload = wp_upload_bits($filename, null, $decoded);
2383 -
2384 - if (!empty($upload['error'])) {
2385 - return new \WP_Error('upload_failed', $upload['error']);
2386 - }
2387 -
2388 - $attach_id = wp_insert_attachment([
2389 - 'post_mime_type' => $mime_type,
2390 - 'post_title' => $prefix,
2391 - 'post_content' => '',
2392 - 'post_status' => 'inherit',
2393 - ], $upload['file']);
2394 -
2395 - if (is_wp_error($attach_id)) {
2396 - return $attach_id;
2397 - }
2398 -
2399 - require_once ABSPATH . 'wp-admin/includes/image.php';
2400 - $metadata = wp_generate_attachment_metadata($attach_id, $upload['file']);
2401 - wp_update_attachment_metadata($attach_id, $metadata);
2402 -
2403 - return esc_url_raw(wp_get_attachment_url($attach_id));
2404 -}
2405 -
2406 -private function mxchat_generate_openai_image($prompt, $api_key, $model = 'gpt-image-1', $timeout = 60) {
1783 +private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
2407 1784 $api_url = 'https://api.openai.com/v1/images/generations';
2408 1785 $body = json_encode([
2409 - 'prompt' => sanitize_text_field($prompt),
2410 - 'n' => 1,
2411 - 'size' => '1024x1024',
2412 - 'quality' => 'medium',
2413 - 'output_format' => 'png',
2414 - 'model' => sanitize_text_field($model),
1786 + 'prompt' => sanitize_text_field($prompt),
1787 + 'n' => 1,
1788 + 'size' => '1024x1024',
1789 + 'model' => sanitize_text_field($model),
2415 1790 ]);
2416 1791
2417 1792 $args = [
2418 - 'body' => $body,
1793 + 'body' => $body,
2419 1794 'headers' => [
2420 - 'Content-Type' => 'application/json',
1795 + 'Content-Type' => 'application/json',
2421 1796 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
2422 1797 ],
2423 - 'method' => 'POST',
1798 + 'method' => 'POST',
2424 1799 'timeout' => absint($timeout),
2425 1800 ];
2426 1801
2427 1802 $response = wp_remote_post($api_url, $args);
@@ -2426,67 +1801,22 @@
2426 1801
2427 1802 $response = wp_remote_post($api_url, $args);
2428 1803
2429 1804 if (is_wp_error($response)) {
1805 + //error_log("DALL-E request failed: " . $response->get_error_message());
2430 1806 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2431 1807 }
2432 1808
2433 1809 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2434 1810
2435 - $b64 = $response_body['data'][0]['b64_json'] ?? $response_body['data'][0]['b64'] ?? null;
2436 - if ($b64) {
2437 - $saved_url = $this->mxchat_save_generated_image($b64, 'image/png', 'mxchat-openai');
2438 - if (is_wp_error($saved_url)) {
2439 - return ['error' => $saved_url->get_error_message()];
2440 - }
2441 - return ['imageUrl' => $saved_url];
1811 + if (isset($response_body['data'][0]['url'])) {
1812 + return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
2442 1813 } else {
1814 + //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
2443 1815 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2444 1816 }
2445 1817 }
2446 1818
2447 -private function mxchat_generate_imagen_image($prompt, $api_key, $timeout = 60) {
2448 - $api_url = 'https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict';
2449 -
2450 - $body = json_encode([
2451 - 'instances' => [['prompt' => sanitize_text_field($prompt)]],
2452 - 'parameters' => [
2453 - 'sampleCount' => 1,
2454 - 'aspectRatio' => '1:1',
2455 - ],
2456 - ]);
2457 -
2458 - $args = [
2459 - 'body' => $body,
2460 - 'headers' => [
2461 - 'Content-Type' => 'application/json',
2462 - 'x-goog-api-key' => sanitize_text_field($api_key),
2463 - ],
2464 - 'method' => 'POST',
2465 - 'timeout' => absint($timeout),
2466 - ];
2467 -
2468 - $response = wp_remote_post($api_url, $args);
2469 -
2470 - if (is_wp_error($response)) {
2471 - return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2472 - }
2473 -
2474 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
2475 -
2476 - $b64 = $response_body['predictions'][0]['bytesBase64Encoded'] ?? $response_body['predictions'][0]['imageBytes'] ?? null;
2477 - if ($b64) {
2478 - $mime = $response_body['predictions'][0]['mimeType'] ?? 'image/png';
2479 - $saved_url = $this->mxchat_save_generated_image($b64, $mime, 'mxchat-gemini');
2480 - if (is_wp_error($saved_url)) {
2481 - return ['error' => $saved_url->get_error_message()];
2482 - }
2483 - return ['imageUrl' => $saved_url];
2484 - } else {
2485 - return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2486 - }
2487 -}
2488 -
2489 1819 /**
2490 1820 * Handle web search requests.
2491 1821 *
2492 1822 * Sends the refined search query to the Brave Search API and uses the
@@ -2535,10 +1865,10 @@
2535 1865 $transient_key = 'mxchat_search_' . md5($refined_search_query);
2536 1866 $results = get_transient($transient_key);
2537 1867
2538 1868 if (false === $results) {
2539 - // SECURITY FIX: Changed to wp_safe_remote_get
2540 - $response = wp_safe_remote_get(
1869 + // Fetch new results from the Brave Search API
1870 + $response = wp_remote_get(
2541 1871 $api_url,
2542 1872 array(
2543 1873 'headers' => array(
2544 1874 'Accept' => 'application/json',
@@ -2677,10 +2007,9 @@
2677 2007 ],
2678 2008 'timeout' => 10,
2679 2009 ];
2680 2010
2681 - // SECURITY FIX: Changed to wp_safe_remote_get
2682 - $response = wp_safe_remote_get($api_url, $args);
2011 + $response = wp_remote_get($api_url, $args);
2683 2012
2684 2013 if (is_wp_error($response)) {
2685 2014 return array(
2686 2015 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
@@ -2753,9 +2082,9 @@
2753 2082 $system_prompt = esc_html__("Interpret the user's request to provide only the essential keywords or phrases for image searching. Remove conversational language, politeness, or extra context. Return a concise search query that doesn't lose any of the original meaning.", 'mxchat');
2754 2083
2755 2084 // Get options and determine the selected model
2756 2085 $options = $this->options ?? get_option('mxchat_options');
2757 - $selected_model = isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest';
2086 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
2758 2087
2759 2088 // Extract model prefix to determine the provider
2760 2089 $model_parts = explode('-', $selected_model);
2761 2090 $provider = strtolower($model_parts[0]);
@@ -2803,9 +2132,9 @@
2803 2132
2804 2133 /**
2805 2134 * Interpret query using OpenAI models
2806 2135 */
2807 -private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-5.1-chat-latest') {
2136 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
2808 2137 $url = 'https://api.openai.com/v1/chat/completions';
2809 2138 $args = [
2810 2139 'headers' => [
2811 2140 'Authorization' => 'Bearer ' . $api_key,
@@ -2876,13 +2205,13 @@
2876 2205 /**
2877 2206 * Interpret query using Gemini models
2878 2207 */
2879 2208 private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
2880 - // Use v1beta for preview models, v1 for stable models
2881 - $api_version = (strpos($model, 'preview') !== false || strpos($model, 'exp') !== false) ? 'v1beta' : 'v1';
2882 -
2883 - $url = "https://generativelanguage.googleapis.com/{$api_version}/models/{$model}:generateContent?key=" . urlencode($api_key);
2209 + // Strip "gemini-" prefix for the API
2210 + $model_version = str_replace('gemini-', '', $model);
2884 2211
2212 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
2213 +
2885 2214 $args = [
2886 2215 'headers' => [
2887 2216 'Content-Type' => 'application/json',
2888 2217 ],
@@ -3078,9 +2407,9 @@
3078 2407 }
3079 2408
3080 2409
3081 2410 /**
3082 - * Enhanced fetch_and_split_pdf_pages with SSRF protection
2411 + * Enhanced fetch_and_split_pdf_pages with detailed debugging
3083 2412 */
3084 2413 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
3085 2414 // CLEAR DEBUG LOGGING
3086 2415 //error_log("=== MXCHAT PDF PROCESSING START ===");
@@ -3135,19 +2464,10 @@
3135 2464 // (I'll include the key parts with debug logging)
3136 2465
3137 2466 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
3138 2467 //error_log("Downloading PDF from URL...");
3139 -
3140 - // SECURITY FIX: Validate URL before processing
3141 - if (!$this->mxchat_is_safe_pdf_url($pdf_source)) {
3142 - //error_log("❌ SECURITY: Blocked unsafe PDF URL");
3143 - return false;
3144 - }
3145 -
3146 2468 $temp_file = wp_tempnam($pdf_source);
3147 -
3148 - // SECURITY FIX: Changed from wp_remote_get to wp_safe_remote_get
3149 - $response = wp_safe_remote_get($pdf_source, [
2469 + $response = wp_remote_get($pdf_source, [
3150 2470 'timeout' => 60,
3151 2471 'headers' => ['User-Agent' => 'MxChat PDF Processor']
3152 2472 ]);
3153 2473
@@ -3156,14 +2476,9 @@
3156 2476 //error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
3157 2477 return false;
3158 2478 }
3159 2479
3160 - global $wp_filesystem;
3161 - if (empty($wp_filesystem)) {
3162 - require_once ABSPATH . 'wp-admin/includes/file.php';
3163 - WP_Filesystem();
3164 - }
3165 - $wp_filesystem->put_contents($temp_file, wp_remote_retrieve_body($response), FS_CHMOD_FILE);
2480 + file_put_contents($temp_file, wp_remote_retrieve_body($response));
3166 2481 //error_log("✅ PDF downloaded successfully");
3167 2482 } else {
3168 2483 $temp_file = $pdf_source;
3169 2484 //error_log("Using local PDF file: " . $temp_file);
@@ -3170,9 +2485,8 @@
3170 2485 }
3171 2486
3172 2487 // Parse PDF
3173 2488 //error_log("Parsing PDF with basic parser...");
3174 - mxchat_load_pdf_parser();
3175 2489 $parser = new \Smalot\PdfParser\Parser();
3176 2490 $pdf = $parser->parseFile($temp_file);
3177 2491 $pages = $pdf->getPages();
3178 2492
@@ -3235,33 +2549,8 @@
3235 2549 return false;
3236 2550 }
3237 2551 }
3238 2552
3239 -
3240 -/**
3241 - * Validate PDF URL for security
3242 - * Prevents SSRF attacks by blocking dangerous URLs
3243 - */
3244 -
3245 -private function mxchat_is_safe_pdf_url($url) {
3246 - // Use WordPress core function for comprehensive validation
3247 - // This blocks localhost, private IPs, and reserved IP ranges
3248 - $validated_url = wp_http_validate_url($url);
3249 -
3250 - if ($validated_url === false) {
3251 - return false;
3252 - }
3253 -
3254 - // Additional check: only allow HTTP/HTTPS schemes
3255 - $parsed = parse_url($url);
3256 - if (!isset($parsed['scheme']) || !in_array($parsed['scheme'], ['http', 'https'], true)) {
3257 - return false;
3258 - }
3259 -
3260 - return true;
3261 -}
3262 -
3263 -
3264 2553 private function mxchat_clean_text($text) {
3265 2554 // Remove excessive whitespace
3266 2555 $text = preg_replace('/\s+/', ' ', $text);
3267 2556
@@ -3300,10 +2589,9 @@
3300 2589 }
3301 2590
3302 2591 return [];
3303 2592 }
3304 -
3305 -
2593 +// Add this to your class
3306 2594 public function handle_pdf_upload() {
3307 2595 check_ajax_referer('mxchat_chat_nonce', 'nonce');
3308 2596
3309 2597 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
@@ -3310,29 +2598,12 @@
3310 2598 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
3311 2599 return;
3312 2600 }
3313 2601
3314 - // SECURITY FIX: Check if PDF uploads are enabled in settings
3315 - $options = get_option('mxchat_options', array());
3316 - $show_pdf_button = isset($options['show_pdf_upload_button']) ? $options['show_pdf_upload_button'] : 'on';
3317 -
3318 - if ($show_pdf_button !== 'on') {
3319 - wp_send_json_error(esc_html__('PDF uploads are currently disabled.', 'mxchat'));
3320 - return;
3321 - }
3322 -
3323 2602 $file = $_FILES['pdf_file'];
3324 2603 $session_id = sanitize_text_field($_POST['session_id']);
3325 2604 $original_filename = sanitize_text_field($file['name']);
3326 2605
3327 - // Update session owner if it changed (e.g. IP changed due to network switch)
3328 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
3329 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
3330 -
3331 - if (!$session_owner || $session_owner !== $current_user_identifier) {
3332 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
3333 - }
3334 -
3335 2606 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
3336 2607 if ($file_type['type'] !== 'application/pdf') {
3337 2608 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
3338 2609 return;
@@ -3338,12 +2609,9 @@
3338 2609 return;
3339 2610 }
3340 2611
3341 2612 $upload_dir = wp_upload_dir();
3342 -
3343 - // SECURITY FIX: Generate random filename without exposing session_id
3344 - $random_string = wp_generate_password(20, false, false); // 20 char alphanumeric string
3345 - $pdf_filename = 'mxchat_' . $random_string . '_' . time() . '.pdf';
2613 + $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
3346 2614 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
3347 2615
3348 2616 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
3349 2617 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
@@ -3374,9 +2642,8 @@
3374 2642 return;
3375 2643 }
3376 2644
3377 2645 if (!empty($embeddings)) {
3378 - // Store the mapping between session and the random filename
3379 2646 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
3380 2647 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
3381 2648 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
3382 2649 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
@@ -3420,8 +2687,10 @@
3420 2687 wp_die();
3421 2688 }
3422 2689
3423 2690
2691 +
2692 +
3424 2693 function mxchat_fetch_new_messages() {
3425 2694 $session_id = sanitize_text_field($_POST['session_id']);
3426 2695 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
3427 2696 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
@@ -3434,31 +2703,14 @@
3434 2703 }
3435 2704
3436 2705 $history = get_option("mxchat_history_{$session_id}", []);
3437 2706
3438 - //error_log("MxChat WhatsApp DEBUG: Fetch new messages for session {$session_id}");
3439 - //error_log("MxChat WhatsApp DEBUG: last_seen_id = " . var_export($last_seen_id, true));
3440 - //error_log("MxChat WhatsApp DEBUG: History count = " . count($history));
3441 - //error_log("MxChat WhatsApp DEBUG: Full history = " . print_r($history, true));
3442 -
3443 2707 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
3444 - //error_log("MxChat WhatsApp DEBUG: Checking message - ID: " . ($message['id'] ?? 'NO_ID') . ", Role: " . ($message['role'] ?? 'NO_ROLE'));
3445 -
3446 2708 // If persistence is enabled, show all new messages
3447 2709 if ($persistence_enabled) {
3448 - $has_id = !empty($message['id']);
3449 - $is_agent = $message['role'] === 'agent';
3450 -
3451 - // If last_seen_id is empty, 'NaN', or invalid, show all agent messages
3452 - if (empty($last_seen_id) || $last_seen_id === 'NaN' || $last_seen_id === 'undefined') {
3453 - $is_newer = true;
3454 - } else {
3455 - $is_newer = strcmp($message['id'] ?? '', $last_seen_id) > 0;
3456 - }
3457 -
3458 - //error_log("MxChat WhatsApp DEBUG: has_id={$has_id}, is_newer={$is_newer}, is_agent={$is_agent}");
3459 -
3460 - return $has_id && $is_newer && $is_agent;
2710 + return !empty($message['id']) &&
2711 + strcmp($message['id'], $last_seen_id) > 0 &&
2712 + $message['role'] === 'agent';
3461 2713 }
3462 2714
3463 2715 // If persistence is disabled, only show messages after initial timestamp
3464 2716 return !empty($message['id']) &&
@@ -3465,16 +2717,12 @@
3465 2717 $message['role'] === 'agent' &&
3466 2718 $message['timestamp'] > $initial_timestamp;
3467 2719 });
3468 2720
3469 - //error_log("MxChat WhatsApp DEBUG: Filtered messages count = " . count($new_messages));
2721 + //error_log(esc_html__("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id", 'mxchat'));
3470 2722
3471 - // Include current chat mode so frontend can detect agent→AI transitions
3472 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
3473 -
3474 2723 wp_send_json_success([
3475 - 'new_messages' => array_values($new_messages),
3476 - 'chat_mode' => $chat_mode
2724 + 'new_messages' => array_values($new_messages)
3477 2725 ]);
3478 2726 wp_die();
3479 2727 }
3480 2728 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
@@ -3759,393 +3007,9 @@
3759 3007
3760 3008 //error_log("[DEBUG] Generated channel name: {$channel_name}");
3761 3009 return $channel_name;
3762 3010 }
3763 -
3764 -/**
3765 - * Telegram Live Agent Handover
3766 - * Creates a forum topic in the Telegram group and notifies agents
3767 - */
3768 -public function mxchat_telegram_live_agent_handover($message, $user_id, $session_id) {
3769 - // Check if Telegram agents are available
3770 - $telegram_available = $this->options['telegram_status'] ?? 'off';
3771 - if ($telegram_available !== 'on') {
3772 - $away_message = $this->options['telegram_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
3773 - $this->fallbackResponse = [
3774 - 'text' => $away_message,
3775 - 'html' => '',
3776 - 'images' => [],
3777 - 'chat_mode' => 'ai'
3778 - ];
3779 - wp_send_json([
3780 - 'text' => $away_message,
3781 - 'html' => '',
3782 - 'chat_mode' => 'ai',
3783 - 'session_id' => $session_id
3784 - ]);
3785 - wp_die();
3786 - }
3787 -
3788 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
3789 - $telegram_group_id = $this->options['telegram_group_id'] ?? '';
3790 -
3791 - if (empty($telegram_bot_token) || empty($telegram_group_id)) {
3792 - return false;
3793 - }
3794 -
3795 - // Check if topic already exists for this session
3796 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
3797 -
3798 - if (empty($topic_id)) {
3799 - // Generate topic name
3800 - $topic_name = $this->generate_telegram_topic_name($session_id);
3801 -
3802 - // Random icon color (Telegram forum topic colors)
3803 - $icon_colors = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, 0xFB6F5F];
3804 - $icon_color = $icon_colors[array_rand($icon_colors)];
3805 -
3806 - // Create forum topic
3807 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/createForumTopic", [
3808 - 'headers' => ['Content-Type' => 'application/json'],
3809 - 'body' => json_encode([
3810 - 'chat_id' => $telegram_group_id,
3811 - 'name' => $topic_name,
3812 - 'icon_color' => $icon_color
3813 - ])
3814 - ]);
3815 -
3816 - if (!is_wp_error($response)) {
3817 - $response_body = wp_remote_retrieve_body($response);
3818 - $response_data = json_decode($response_body, true);
3819 -
3820 - if (isset($response_data['ok']) && $response_data['ok']) {
3821 - $topic_id = $response_data['result']['message_thread_id'];
3822 - update_option("mxchat_telegram_topic_{$session_id}", $topic_id);
3823 - update_option("mxchat_telegram_group_{$session_id}", $telegram_group_id);
3824 - }
3825 - }
3826 -
3827 - if (empty($topic_id)) {
3828 - return false; // Failed to create topic
3829 - }
3830 - }
3831 -
3832 - // Get recent chat history
3833 - $history = get_option("mxchat_history_{$session_id}", []);
3834 - $recent_history = array_slice($history, -5);
3835 -
3836 - // Format conversation context for Telegram (HTML format)
3837 - $conversation_context = "";
3838 - if (!empty($recent_history)) {
3839 - $conversation_context = "<b>Recent Conversation:</b>\n";
3840 - foreach ($recent_history as $hist_message) {
3841 - $role_display = $hist_message['role'] === 'user' ? '👤 User' : '🤖 AI';
3842 - $escaped_content = htmlspecialchars($hist_message['content'], ENT_QUOTES, 'UTF-8');
3843 - $conversation_context .= "{$role_display}: {$escaped_content}\n";
3844 - }
3845 - $conversation_context .= "\n";
3846 - }
3847 -
3848 - // Get user info
3849 - $user_email = get_option("mxchat_email_{$session_id}", 'Not provided');
3850 - $user_name = get_option("mxchat_name_{$session_id}", 'Anonymous');
3851 -
3852 - // Update session mode
3853 - update_option("mxchat_mode_{$session_id}", 'agent');
3854 -
3855 - // Send initial message to topic
3856 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3857 - $topic_message = "🔔 <b>New Live Agent Request</b>\n\n";
3858 - $topic_message .= "<b>Session ID:</b> <code>{$session_id}</code>\n";
3859 - $topic_message .= "<b>User:</b> {$user_name}\n";
3860 - $topic_message .= "<b>Email:</b> {$user_email}\n\n";
3861 -
3862 - if (!empty($conversation_context)) {
3863 - $topic_message .= $conversation_context;
3864 - }
3865 -
3866 - $topic_message .= "<b>Current Message:</b>\n{$escaped_message}\n\n";
3867 - $topic_message .= "<i>Reply in this topic - messages will be sent to the user</i>\n";
3868 - $topic_message .= "<i>Type #close, #end, #disconnect, or #done to end the session</i>";
3869 -
3870 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
3871 - 'headers' => ['Content-Type' => 'application/json'],
3872 - 'body' => json_encode([
3873 - 'chat_id' => $telegram_group_id,
3874 - 'message_thread_id' => $topic_id,
3875 - 'text' => $topic_message,
3876 - 'parse_mode' => 'HTML'
3877 - ])
3878 - ]);
3879 -
3880 - $success_message = $this->options['telegram_notification_message'] ?? "I've notified a support agent. Please allow a moment for them to respond.";
3881 - $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
3882 -
3883 - $this->fallbackResponse = [
3884 - 'text' => $success_message,
3885 - 'html' => '',
3886 - 'images' => [],
3887 - 'chat_mode' => 'agent'
3888 - ];
3889 -
3890 - wp_send_json([
3891 - 'success' => true,
3892 - 'text' => $success_message,
3893 - 'html' => '',
3894 - 'chat_mode' => 'agent',
3895 - 'session_id' => $session_id,
3896 - 'fallbackResponse' => $this->fallbackResponse
3897 - ]);
3898 - wp_die();
3899 -}
3900 -
3901 -/**
3902 - * Generate topic name for Telegram forum
3903 - */
3904 -private function generate_telegram_topic_name($session_id) {
3905 - $name = null;
3906 - $email = null;
3907 -
3908 - // Check logged in user
3909 - if (is_user_logged_in()) {
3910 - $current_user = wp_get_current_user();
3911 - if (!empty($current_user->display_name)) {
3912 - $name = $current_user->display_name;
3913 - }
3914 - if (!empty($current_user->user_email)) {
3915 - $email = $current_user->user_email;
3916 - }
3917 - }
3918 -
3919 - // Check session data
3920 - if (empty($name)) {
3921 - $name = get_option("mxchat_name_{$session_id}");
3922 - }
3923 - if (empty($email)) {
3924 - $email = get_option("mxchat_email_{$session_id}");
3925 - }
3926 -
3927 - // Generate topic name
3928 - $session_suffix = substr($session_id, -6);
3929 -
3930 - if (!empty($name)) {
3931 - // Clean name for topic (max 128 chars in Telegram)
3932 - $clean_name = preg_replace('/[^\p{L}\p{N}\s\-]/u', '', $name);
3933 - $clean_name = trim($clean_name);
3934 - if (strlen($clean_name) > 50) {
3935 - $clean_name = substr($clean_name, 0, 50);
3936 - }
3937 - return "Chat - {$clean_name} ({$session_suffix})";
3938 - } elseif (!empty($email)) {
3939 - // Use email prefix
3940 - $email_prefix = explode('@', $email)[0];
3941 - if (strlen($email_prefix) > 30) {
3942 - $email_prefix = substr($email_prefix, 0, 30);
3943 - }
3944 - return "Chat - {$email_prefix} ({$session_suffix})";
3945 - }
3946 -
3947 - return "Chat - {$session_suffix}";
3948 -}
3949 -
3950 -/**
3951 - * Send user message to Telegram agent
3952 - */
3953 -public function mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id) {
3954 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
3955 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
3956 - $group_id = get_option("mxchat_telegram_group_{$session_id}", '');
3957 -
3958 - if (empty($telegram_bot_token) || empty($topic_id) || empty($group_id)) {
3959 - return false;
3960 - }
3961 -
3962 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3963 - $user_message = "👤 <b>User:</b> {$escaped_message}";
3964 -
3965 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
3966 - 'headers' => ['Content-Type' => 'application/json'],
3967 - 'body' => json_encode([
3968 - 'chat_id' => $group_id,
3969 - 'message_thread_id' => $topic_id,
3970 - 'text' => $user_message,
3971 - 'parse_mode' => 'HTML'
3972 - ])
3973 - ]);
3974 -
3975 - return !is_wp_error($response);
3976 -}
3977 -
3978 -/**
3979 - * Handle incoming Telegram webhook
3980 - */
3981 -public function handle_telegram_webhook(WP_REST_Request $request) {
3982 - $body = $request->get_body();
3983 - $data = json_decode($body, true);
3984 -
3985 - //error_log('[MxChat Telegram DEBUG] Webhook received: ' . $body);
3986 -
3987 - // Handle message events from forum topics
3988 - if (isset($data['message'])) {
3989 - $message_data = $data['message'];
3990 -
3991 - // Skip if not from a forum topic
3992 - if (!isset($message_data['message_thread_id'])) {
3993 - //error_log('[MxChat Telegram DEBUG] Skipped: No message_thread_id (not a forum topic message)');
3994 - return new WP_REST_Response(['ok' => true]);
3995 - }
3996 -
3997 - // Skip bot messages
3998 - if (isset($message_data['from']['is_bot']) && $message_data['from']['is_bot']) {
3999 - //error_log('[MxChat Telegram DEBUG] Skipped: Message from bot');
4000 - return new WP_REST_Response(['ok' => true]);
4001 - }
4002 -
4003 - $chat_id = $message_data['chat']['id'] ?? '';
4004 - $topic_id = $message_data['message_thread_id'];
4005 - $message_text = $message_data['text'] ?? '';
4006 - $message_id = $message_data['message_id'] ?? '';
4007 - $from = $message_data['from'] ?? [];
4008 - $agent_name = trim(($from['first_name'] ?? '') . ' ' . ($from['last_name'] ?? ''));
4009 - if (empty($agent_name)) {
4010 - $agent_name = $from['username'] ?? 'Agent';
4011 - }
4012 -
4013 - //error_log("[MxChat Telegram DEBUG] Parsed: chat_id={$chat_id}, topic_id={$topic_id}, agent={$agent_name}, text={$message_text}");
4014 -
4015 - // Skip empty messages
4016 - if (empty($message_text)) {
4017 - //error_log('[MxChat Telegram DEBUG] Skipped: Empty message text');
4018 - return new WP_REST_Response(['ok' => true]);
4019 - }
4020 -
4021 - // Find session ID by topic ID - cast to string for comparison
4022 - global $wpdb;
4023 - $topic_id_str = strval($topic_id);
4024 - $session_option = $wpdb->get_var(
4025 - $wpdb->prepare(
4026 - "SELECT option_name FROM {$wpdb->options}
4027 - WHERE option_name LIKE %s
4028 - AND option_value = %s",
4029 - 'mxchat_telegram_topic_%',
4030 - $topic_id_str
4031 - )
4032 - );
4033 -
4034 - //error_log("[MxChat Telegram DEBUG] Looking for topic_id={$topic_id_str} in options, found: " . ($session_option ?: 'NULL'));
4035 -
4036 - if ($session_option) {
4037 - $session_id = str_replace('mxchat_telegram_topic_', '', $session_option);
4038 - //error_log("[MxChat Telegram DEBUG] Session ID: {$session_id}");
4039 -
4040 - // Verify the group ID matches
4041 - $stored_group_id = get_option("mxchat_telegram_group_{$session_id}", '');
4042 - //error_log("[MxChat Telegram DEBUG] Stored group_id={$stored_group_id}, received chat_id={$chat_id}");
4043 -
4044 - if (strval($stored_group_id) != strval($chat_id)) {
4045 - //error_log('[MxChat Telegram DEBUG] Skipped: Group ID mismatch');
4046 - return new WP_REST_Response(['ok' => true]);
4047 - }
4048 -
4049 - // Check for closure commands
4050 - $lower_text = strtolower(trim($message_text));
4051 - if (in_array($lower_text, ['#close', '#end', '#disconnect', '#done'])) {
4052 - //error_log("[MxChat Telegram DEBUG] Closure command received: {$lower_text}");
4053 - // End the live agent session
4054 - update_option("mxchat_mode_{$session_id}", 'ai');
4055 -
4056 - // Save disconnect message
4057 - $disconnect_message = "Live agent session ended. You're now chatting with the AI assistant.";
4058 - $this->mxchat_save_chat_message($session_id, 'bot', $disconnect_message);
4059 -
4060 - // Notify in Telegram
4061 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4062 - if (!empty($telegram_bot_token)) {
4063 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4064 - 'headers' => ['Content-Type' => 'application/json'],
4065 - 'body' => json_encode([
4066 - 'chat_id' => $chat_id,
4067 - 'message_thread_id' => $topic_id,
4068 - 'text' => "✅ Session closed. User returned to AI chatbot.",
4069 - 'parse_mode' => 'HTML'
4070 - ])
4071 - ]);
4072 -
4073 - // Optionally close the topic
4074 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/closeForumTopic", [
4075 - 'headers' => ['Content-Type' => 'application/json'],
4076 - 'body' => json_encode([
4077 - 'chat_id' => $chat_id,
4078 - 'message_thread_id' => $topic_id
4079 - ])
4080 - ]);
4081 - }
4082 -
4083 - return new WP_REST_Response(['ok' => true]);
4084 - }
4085 -
4086 - // Deduplicate messages
4087 - $message_key = md5($session_id . $message_id . $message_text);
4088 - $processed_messages = get_transient('mxchat_telegram_messages_' . $session_id) ?: [];
4089 -
4090 - if (in_array($message_key, $processed_messages)) {
4091 - //error_log('[MxChat Telegram DEBUG] Skipped: Duplicate message');
4092 - return new WP_REST_Response(['ok' => true]);
4093 - }
4094 -
4095 - $processed_messages[] = $message_key;
4096 - if (count($processed_messages) > 50) {
4097 - $processed_messages = array_slice($processed_messages, -50);
4098 - }
4099 - set_transient('mxchat_telegram_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4100 -
4101 - // Save the agent message - format with agent name prefix for proper parsing
4102 - $formatted_message = "Agent: {$agent_name} - {$message_text}";
4103 - //error_log("[MxChat Telegram DEBUG] Saving agent message: {$formatted_message}");
4104 -
4105 - $this->mxchat_save_chat_message($session_id, 'agent', $formatted_message);
4106 -
4107 - // Verify the message was saved to history
4108 - $history = get_option("mxchat_history_{$session_id}", []);
4109 - $last_message = end($history);
4110 - //error_log("[MxChat Telegram DEBUG] History after save - count: " . count($history) . ", last message role: " . ($last_message['role'] ?? 'none'));
4111 -
4112 - // Send confirmation back to Telegram
4113 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4114 - if (!empty($telegram_bot_token)) {
4115 - $confirm_key = 'mxchat_telegram_confirm_' . $message_key;
4116 - if (!get_transient($confirm_key)) {
4117 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4118 - 'headers' => ['Content-Type' => 'application/json'],
4119 - 'body' => json_encode([
4120 - 'chat_id' => $chat_id,
4121 - 'message_thread_id' => $topic_id,
4122 - 'text' => "✅ <i>Message sent to user</i>",
4123 - 'parse_mode' => 'HTML',
4124 - 'reply_to_message_id' => $message_id
4125 - ])
4126 - ]);
4127 - set_transient($confirm_key, true, 300);
4128 - }
4129 - }
4130 - } else {
4131 - //error_log("[MxChat Telegram DEBUG] No session found for topic_id={$topic_id}");
4132 - }
4133 - } else {
4134 - //error_log('[MxChat Telegram DEBUG] No message in webhook data');
4135 - }
4136 -
4137 - return new WP_REST_Response(['ok' => true]);
4138 -}
4139 -
4140 3011 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
4141 - // Check if this is a Telegram agent session
4142 - $telegram_topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4143 - if (!empty($telegram_topic_id)) {
4144 - return $this->mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id);
4145 - }
4146 -
4147 - // Otherwise, try Slack
4148 3012 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4149 3013 $channel_id = get_option("mxchat_channel_{$session_id}", '');
4150 3014
4151 3015 if (empty($slack_bot_token) || empty($channel_id)) {
@@ -4371,33 +3235,33 @@
4371 3235
4372 3236 $channel_id = $event['channel'];
4373 3237 $message_text = $event['text'] ?? '';
4374 3238 $message_ts = $event['ts'] ?? '';
4375 -
3239 +
4376 3240 // Find session ID by looking for matching channel
4377 3241 global $wpdb;
4378 3242 $session_option = $wpdb->get_var(
4379 3243 $wpdb->prepare(
4380 - "SELECT option_name FROM {$wpdb->options}
4381 - WHERE option_name LIKE 'mxchat_channel_%'
3244 + "SELECT option_name FROM {$wpdb->options}
3245 + WHERE option_name LIKE 'mxchat_channel_%'
4382 3246 AND option_value = %s",
4383 3247 $channel_id
4384 3248 )
4385 3249 );
4386 -
3250 +
4387 3251 if ($session_option) {
4388 3252 $session_id = str_replace('mxchat_channel_', '', $session_option);
4389 -
3253 +
4390 3254 // Create a unique key for this specific message
4391 3255 $message_key = md5($session_id . $message_ts . $message_text);
4392 3256 $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
4393 -
3257 +
4394 3258 // Check if we've already processed this exact message
4395 3259 if (in_array($message_key, $processed_messages)) {
4396 3260 //error_log("Duplicate message detected for session $session_id");
4397 3261 return new WP_REST_Response(['ok' => true]);
4398 3262 }
4399 -
3263 +
4400 3264 // Add to processed messages
4401 3265 $processed_messages[] = $message_key;
4402 3266 // Keep only last 50 messages per session
4403 3267 if (count($processed_messages) > 50) {
@@ -4403,46 +3267,14 @@
4403 3267 if (count($processed_messages) > 50) {
4404 3268 $processed_messages = array_slice($processed_messages, -50);
4405 3269 }
4406 3270 set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4407 -
4408 - $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4409 -
4410 - // Handle agent ending the chat — transfer back to AI
4411 - // Format: "!endchat" or "!endchat <custom message to user>"
4412 - if (preg_match('/^!endchat\b/i', trim($message_text))) {
4413 - update_option("mxchat_mode_{$session_id}", 'ai');
4414 -
4415 - // Extract custom message after !endchat, or use empty string
4416 - $custom_message = trim(preg_replace('/^!endchat\s*/i', '', trim($message_text)));
4417 -
4418 - // Send the agent's custom farewell message if provided
4419 - if (!empty($custom_message)) {
4420 - $this->mxchat_save_chat_message($session_id, 'agent', $custom_message);
4421 - }
4422 -
4423 - // Confirm in Slack channel
4424 - if (!empty($slack_bot_token)) {
4425 - wp_remote_post('https://slack.com/api/chat.postMessage', [
4426 - 'headers' => [
4427 - 'Content-Type' => 'application/json',
4428 - 'Authorization' => 'Bearer ' . $slack_bot_token
4429 - ],
4430 - 'body' => json_encode([
4431 - 'channel' => $channel_id,
4432 - 'text' => "✅ *Chat ended.* User has been transferred back to AI mode.",
4433 - 'mrkdwn' => true
4434 - ])
4435 - ]);
4436 - }
4437 -
4438 - return new WP_REST_Response(['ok' => true]);
4439 - }
4440 -
3271 +
4441 3272 // Save the agent message
4442 3273 $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
4443 -
3274 +
4444 3275 // Send confirmation back to Slack (only once)
3276 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4445 3277 if (!empty($slack_bot_token)) {
4446 3278 // Use a transient to prevent duplicate confirmations
4447 3279 $confirm_key = 'mxchat_confirm_' . $message_key;
4448 3280 if (!get_transient($confirm_key)) {
@@ -4452,9 +3284,9 @@
4452 3284 'Authorization' => 'Bearer ' . $slack_bot_token
4453 3285 ],
4454 3286 'body' => json_encode([
4455 3287 'channel' => $channel_id,
4456 - 'text' => "✅ _Message sent to user_",
3288 + 'text' => "✅ _Message sent to user_",
4457 3289 'thread_ts' => $event['ts'] // Reply in thread
4458 3290 ])
4459 3291 ]);
4460 3292 // Set transient to prevent duplicate confirmations
@@ -4693,44 +3525,26 @@
4693 3525 }
4694 3526 }
4695 3527
4696 3528
4697 -private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default', $user_query = '') {
4698 - //error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
4699 -
4700 - // Check for OpenAI Vector Store first (takes priority when enabled)
4701 - $bot_vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
4702 -
4703 - if ($bot_vectorstore_config['use_vectorstore']) {
4704 - // Get current model to verify it's an OpenAI model
4705 - $bot_options = $this->get_bot_options($bot_id);
4706 - $mxchat_options = get_option('mxchat_options', array());
4707 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
4708 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
4709 -
4710 - if ($this->is_openai_chat_model($selected_model)) {
4711 - //error_log("MXCHAT DEBUG: Using OpenAI Vector Store for knowledge retrieval");
4712 - return $this->find_relevant_content_openai_vectorstore($user_query, $bot_id, $bot_vectorstore_config);
4713 - } else {
4714 - //error_log("MXCHAT DEBUG: Vector Store enabled but model is not OpenAI (" . $selected_model . "), skipping Vector Store");
4715 - }
4716 - }
4717 -
3529 +private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default') {
3530 + error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
3531 +
4718 3532 // Get bot-specific Pinecone configuration
4719 3533 $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
4720 -
3534 +
4721 3535 // Debug: Log the Pinecone configuration
4722 - //error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
4723 - //error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
4724 - //error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
4725 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
4726 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
4727 -
3536 + error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
3537 + error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
3538 + error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
3539 + error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
3540 + error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
3541 +
4728 3542 // Determine whether to use Pinecone based on bot configuration
4729 3543 $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
3544 +
3545 + error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
4730 3546
4731 - //error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
4732 -
4733 3547 if ($use_pinecone) {
4734 3548 return $this->find_relevant_content_pinecone($user_embedding, $bot_id, $bot_pinecone_config);
4735 3549 } else {
4736 3550 return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
@@ -4739,483 +3553,233 @@
4739 3553
4740 3554 private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
4741 3555 global $wpdb;
4742 3556 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3557 + $cache_key = 'mxchat_system_prompt_embeddings_' . $bot_id; // Bot-specific cache key
3558 + $batch_size = 500;
3559 +
4743 3560 // Initialize similarity analysis storage
4744 3561 $this->last_similarity_analysis = [
4745 3562 'knowledge_base_type' => 'WordPress Database',
4746 - 'bot_id' => $bot_id,
3563 + 'bot_id' => $bot_id, // Track which bot is being used
4747 3564 'top_matches' => [],
4748 3565 'threshold_used' => 0,
4749 3566 'total_checked' => 0
4750 3567 ];
4751 3568
4752 - // NEW: Initialize valid URLs array
4753 - $valid_urls = [];
4754 -
4755 - // Get bot-specific options for similarity threshold
3569 + // Get bot-specific options for similarity threshold
4756 3570 $bot_options = $this->get_bot_options($bot_id);
4757 3571 $current_options = !empty($bot_options) ? $bot_options : $this->options;
4758 3572
4759 - // Get knowledge manager instance for role checking
4760 - $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3573 + // Retrieve embeddings from cache or database
3574 + $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3575 + if ($embeddings === false) {
3576 + // Cache miss - load embeddings from database WITH CONTENT and ROLE RESTRICTION for testing
3577 + $embeddings = [];
3578 + $offset = 0;
4761 3579
4762 - // Get base similarity threshold from bot options or default options
4763 - $similarity_threshold = isset($current_options['similarity_threshold'])
4764 - ? ((int) $current_options['similarity_threshold']) / 100
4765 - : 0.35;
4766 - $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3580 + do {
3581 + // Add bot_id filter if not default and if bot_metadata column exists
3582 + $bot_filter = '';
3583 + if ($bot_id !== 'default') {
3584 + // Check if bot_metadata column exists
3585 + $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
3586 + if ($column_exists) {
3587 + $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
3588 + }
3589 + }
4767 3590
4768 - // Precompute bot_filter once, outside the streaming loop
4769 - $bot_filter = '';
4770 - if ($bot_id !== 'default') {
4771 - $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
4772 - if ($column_exists) {
4773 - $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
4774 - }
4775 - }
3591 + $query = $wpdb->prepare(
3592 + "SELECT id, embedding_vector, article_content, source_url, role_restriction
3593 + FROM {$system_prompt_table}
3594 + WHERE 1=1 {$bot_filter}
3595 + LIMIT %d OFFSET %d",
3596 + $batch_size,
3597 + $offset
3598 + );
4776 3599
4777 - // ===== STREAMING TOP-K PASS =====
4778 - // Stream rows in small batches, compute cosine similarity per row, and keep only:
4779 - // - top 10 by raw similarity (for the testing/debug display panel)
4780 - // - candidates above threshold with access (capped) for context assembly
4781 - // This bounds peak memory regardless of knowledge base size and avoids loading
4782 - // article_content for every row. article_content is fetched in Phase 2 for winners only.
4783 - $batch_size = 250;
4784 - $max_candidates = 200; // safety cap, well above rag_sources_limit * max_chunks_per_source
4785 - $top_display = [];
4786 - $candidates = [];
4787 - $total_checked = 0;
4788 - $offset = 0;
3600 + $batch = $wpdb->get_results($query);
3601 + if (empty($batch)) {
3602 + break;
3603 + }
4789 3604
4790 - do {
4791 - $batch = $wpdb->get_results($wpdb->prepare(
4792 - "SELECT id, embedding_vector, source_url, role_restriction
4793 - FROM {$system_prompt_table}
4794 - WHERE 1=1 {$bot_filter}
4795 - LIMIT %d OFFSET %d",
4796 - $batch_size,
4797 - $offset
4798 - ));
3605 + $embeddings = array_merge($embeddings, $batch);
3606 + $offset += $batch_size;
3607 + unset($batch);
3608 + } while (true);
4799 3609
4800 - if (empty($batch)) {
4801 - break;
3610 + if (empty($embeddings)) {
3611 + return '';
4802 3612 }
3613 +
3614 + // Cache embeddings for future use (but note: this now includes content and role restrictions)
3615 + wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3616 + }
4803 3617
4804 - foreach ($batch as $row) {
4805 - $database_embedding = $row->embedding_vector
4806 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
4807 - : null;
3618 + // Get knowledge manager instance for role checking
3619 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
4808 3620
4809 - if (!is_array($database_embedding) || !is_array($user_embedding)) {
4810 - unset($database_embedding);
4811 - continue;
4812 - }
4813 -
3621 + // Get base similarity threshold from bot options or default options
3622 + $similarity_threshold = isset($current_options['similarity_threshold'])
3623 + ? ((int) $current_options['similarity_threshold']) / 100
3624 + : 0.35;
3625 +
3626 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3627 +
3628 + // Calculate similarities and build results array
3629 + $all_similarities = [];
3630 + $relevant_results = [];
3631 +
3632 + foreach ($embeddings as $embedding) {
3633 + $database_embedding = $embedding->embedding_vector
3634 + ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3635 + : null;
3636 +
3637 + if (is_array($database_embedding) && is_array($user_embedding)) {
4814 3638 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
4815 - unset($database_embedding);
4816 -
4817 - $role_restriction = $row->role_restriction ?? 'public';
3639 +
3640 + // Check role access
3641 + $role_restriction = $embedding->role_restriction ?? 'public';
4818 3642 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
4819 - $source_url = $row->source_url ?? '';
4820 -
4821 - // Maintain top 10 display buffer (insert-if-beats-worst)
4822 - if (count($top_display) < 10) {
4823 - $top_display[] = [
4824 - 'id' => $row->id,
4825 - 'similarity' => $similarity,
4826 - 'source_url' => $source_url,
4827 - 'role_restriction' => $role_restriction,
4828 - 'has_access' => $has_access,
4829 - ];
4830 - usort($top_display, function ($a, $b) {
4831 - return $b['similarity'] <=> $a['similarity'];
4832 - });
4833 - } elseif ($similarity > $top_display[9]['similarity']) {
4834 - $top_display[9] = [
4835 - 'id' => $row->id,
4836 - 'similarity' => $similarity,
4837 - 'source_url' => $source_url,
4838 - 'role_restriction' => $role_restriction,
4839 - 'has_access' => $has_access,
4840 - ];
4841 - usort($top_display, function ($a, $b) {
4842 - return $b['similarity'] <=> $a['similarity'];
4843 - });
3643 +
3644 + // Store ALL similarities for testing (top 10)
3645 + $source_display = '';
3646 + if (!empty($embedding->source_url) && $embedding->source_url !== '#') {
3647 + $source_display = $embedding->source_url;
3648 + } else {
3649 + $content_preview = strip_tags($embedding->article_content ?? '');
3650 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3651 + $source_display = substr(trim($content_preview), 0, 50) . '...';
4844 3652 }
4845 -
4846 - // Track candidates for context assembly (above threshold + has access)
3653 +
3654 + $all_similarities[] = [
3655 + 'document_id' => $embedding->id,
3656 + 'similarity' => $similarity,
3657 + 'similarity_percentage' => round($similarity * 100, 2),
3658 + 'above_threshold' => $similarity >= $similarity_threshold,
3659 + 'source_display' => $source_display,
3660 + 'content_preview' => substr(strip_tags($embedding->article_content ?? ''), 0, 100) . '...',
3661 + 'used_for_context' => false, // Initialize as false, we'll update this later
3662 + 'role_restriction' => $role_restriction, // Include role info for testing
3663 + 'has_access' => $has_access, // Include access info for testing
3664 + 'filtered_out' => !$has_access // Mark if filtered out by role
3665 + ];
3666 +
3667 + // Only consider results above threshold AND with access for actual content retrieval
4847 3668 if ($similarity >= $similarity_threshold && $has_access) {
4848 - $candidates[] = [
4849 - 'id' => $row->id,
4850 - 'similarity' => $similarity,
4851 - 'source_url' => $source_url,
3669 + $relevant_results[] = [
3670 + 'id' => $embedding->id,
3671 + 'similarity' => $similarity
4852 3672 ];
4853 3673 }
4854 -
4855 - $total_checked++;
4856 3674 }
4857 -
4858 - unset($batch);
4859 -
4860 - // Trim candidates periodically to cap memory during long scans
4861 - if (count($candidates) > $max_candidates) {
4862 - usort($candidates, function ($a, $b) {
4863 - return $b['similarity'] <=> $a['similarity'];
4864 - });
4865 - $candidates = array_slice($candidates, 0, $max_candidates);
4866 - }
4867 -
4868 - $offset += $batch_size;
4869 - } while (true);
4870 -
4871 - if ($total_checked === 0) {
4872 - $this->current_valid_urls = [];
4873 - return '';
3675 +
3676 + unset($database_embedding);
4874 3677 }
4875 3678
4876 - // Final candidates sort (best first)
4877 - if (count($candidates) > 1) {
4878 - usort($candidates, function ($a, $b) {
4879 - return $b['similarity'] <=> $a['similarity'];
4880 - });
4881 - }
4882 -
4883 - // ===== PHASE 2: FETCH ARTICLE CONTENT ONLY FOR WINNERS =====
4884 - // Gather unique IDs we actually need (top_display + candidates) and pull
4885 - // article_content in bounded IN() batches. This avoids loading content for
4886 - // every row during the similarity scan.
4887 - $needed_ids = [];
4888 - foreach ($top_display as $item) {
4889 - $needed_ids[$item['id']] = true;
4890 - }
4891 - foreach ($candidates as $item) {
4892 - $needed_ids[$item['id']] = true;
4893 - }
4894 - $needed_ids = array_keys($needed_ids);
4895 -
4896 - $content_map = [];
4897 - if (!empty($needed_ids)) {
4898 - foreach (array_chunk($needed_ids, 250) as $chunk_ids) {
4899 - $placeholders = implode(',', array_fill(0, count($chunk_ids), '%d'));
4900 - $rows = $wpdb->get_results($wpdb->prepare(
4901 - "SELECT id, article_content FROM {$system_prompt_table} WHERE id IN ($placeholders)",
4902 - ...$chunk_ids
4903 - ));
4904 - foreach ($rows as $r) {
4905 - $content_map[$r->id] = $r->article_content;
4906 - }
4907 - unset($rows);
4908 - }
4909 - }
4910 -
4911 - // Build the all_similarities display array from the top 10
4912 - $all_similarities = [];
4913 - foreach ($top_display as $item) {
4914 - $article_content_for_parse = $content_map[$item['id']] ?? '';
4915 - $parsed_for_display = MxChat_Chunker::parse_stored_chunk($article_content_for_parse);
4916 - $is_chunk = $parsed_for_display['is_chunked'];
4917 - $chunk_meta = $parsed_for_display['metadata'];
4918 -
4919 - if (!empty($item['source_url']) && $item['source_url'] !== '#') {
4920 - $source_display = $item['source_url'];
4921 - } else {
4922 - $content_preview = strip_tags($article_content_for_parse);
4923 - $content_preview = preg_replace('/\s+/', ' ', $content_preview);
4924 - $source_display = substr(trim($content_preview), 0, 50) . '...';
4925 - }
4926 -
4927 - $all_similarities[] = [
4928 - 'document_id' => $item['id'],
4929 - 'similarity' => $item['similarity'],
4930 - 'similarity_percentage' => round($item['similarity'] * 100, 2),
4931 - 'above_threshold' => $item['similarity'] >= $similarity_threshold,
4932 - 'source_display' => $source_display,
4933 - 'content_preview' => substr(strip_tags($parsed_for_display['text'] ?? ''), 0, 100) . '...',
4934 - 'used_for_context' => false,
4935 - 'role_restriction' => $item['role_restriction'],
4936 - 'has_access' => $item['has_access'],
4937 - 'filtered_out' => !$item['has_access'],
4938 - 'is_chunk' => $is_chunk,
4939 - 'chunk_index' => $is_chunk ? ($chunk_meta['chunk_index'] ?? 0) : null,
4940 - 'total_chunks' => $is_chunk ? ($chunk_meta['total_chunks'] ?? 1) : null
4941 - ];
4942 - }
4943 -
4944 - // Build url_groups from candidates for chunk reassembly
4945 - $url_groups = array();
4946 - foreach ($candidates as $cand) {
4947 - $article_content = $content_map[$cand['id']] ?? '';
4948 - $parsed = MxChat_Chunker::parse_stored_chunk($article_content);
4949 - $is_chunked = $parsed['is_chunked'];
4950 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
4951 - $text_content = $parsed['text'];
4952 -
4953 - $source_url = $cand['source_url'];
4954 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $cand['id'];
4955 -
4956 - if (!isset($url_groups[$group_key])) {
4957 - $url_groups[$group_key] = array(
4958 - 'source_url' => $source_url,
4959 - 'best_score' => 0,
4960 - 'is_chunked' => $is_chunked,
4961 - 'chunks' => array(),
4962 - 'single_text' => '',
4963 - 'single_id' => null
4964 - );
4965 - }
4966 -
4967 - if ($cand['similarity'] > $url_groups[$group_key]['best_score']) {
4968 - $url_groups[$group_key]['best_score'] = $cand['similarity'];
4969 - }
4970 -
4971 - if ($is_chunked) {
4972 - $url_groups[$group_key]['is_chunked'] = true;
4973 - $url_groups[$group_key]['chunks'][] = array(
4974 - 'id' => $cand['id'],
4975 - 'score' => $cand['similarity'],
4976 - 'chunk_index' => $chunk_index,
4977 - 'text' => $text_content
4978 - );
4979 - } else {
4980 - $url_groups[$group_key]['single_text'] = $text_content;
4981 - $url_groups[$group_key]['single_id'] = $cand['id'];
4982 - }
4983 - }
4984 -
4985 3679 // Sort ALL similarities for testing display (highest first)
4986 3680 usort($all_similarities, function ($a, $b) {
4987 3681 return $b['similarity'] <=> $a['similarity'];
4988 3682 });
4989 -
4990 - // Sort URL groups by best score (highest first)
4991 - uasort($url_groups, function($a, $b) {
4992 - return $b['best_score'] <=> $a['best_score'];
3683 +
3684 + // Sort relevant results by similarity (highest first)
3685 + usort($relevant_results, function ($a, $b) {
3686 + return $b['similarity'] <=> $a['similarity'];
4993 3687 });
4994 -
4995 - // Get RAG sources limit from options (default 6, min 3, max 10)
4996 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
4997 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
4998 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
4999 -
5000 - // Take top N unique URLs based on user setting
5001 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5002 -
5003 - // Track which document IDs are used for context
3688 +
3689 + // Get top 5 results for actual content (standard approach)
3690 + $top_results = array_slice($relevant_results, 0, 5);
3691 +
3692 + // NOW mark which documents are actually used for context
5004 3693 $used_document_ids = [];
5005 - foreach ($top_urls as $group) {
5006 - if ($group['is_chunked']) {
5007 - foreach ($group['chunks'] as $chunk) {
5008 - $used_document_ids[] = $chunk['id'];
5009 - }
5010 - } elseif ($group['single_id']) {
5011 - $used_document_ids[] = $group['single_id'];
5012 - }
3694 + foreach ($top_results as $result) {
3695 + $used_document_ids[] = $result['id'];
5013 3696 }
5014 -
3697 +
5015 3698 // Update the all_similarities array to mark which were actually used
5016 3699 foreach ($all_similarities as &$similarity_item) {
5017 3700 $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
5018 3701 }
5019 -
5020 - // Store top 10 for testing panel
3702 +
3703 + // Store top 10 for testing panel (now with correct used_for_context flags and role info)
5021 3704 $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
5022 - $this->last_similarity_analysis['total_checked'] = $total_checked;
5023 -
3705 + $this->last_similarity_analysis['total_checked'] = count($embeddings);
3706 +
3707 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " top matches for testing");
3708 +
5024 3709 // Initialize final content
5025 3710 $content = '';
5026 - $matches_used = 0;
5027 - $total_chunks_used = 0;
5028 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5029 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5030 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5031 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5032 -
5033 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5034 - // Use fresh options to ensure we get the latest setting value
5035 - $fresh_options = get_option('mxchat_options', []);
5036 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5037 -
5038 - // Build content from top sources
5039 - foreach ($top_urls as $group_key => $group) {
5040 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5041 -
5042 - // Stop if we've hit the total chunk limit
5043 - if ($total_chunks_used >= $max_total_chunks) {
5044 - break;
3711 +
3712 + // Track document IDs to avoid duplicates
3713 + $added_document_ids = [];
3714 +
3715 + // Fetch and format content for each selected result
3716 + foreach ($top_results as $index => $result) {
3717 + if (in_array($result['id'], $added_document_ids)) {
3718 + continue;
5045 3719 }
5046 -
5047 - $full_text = '';
5048 - $chunks_in_this_source = 1; // Default for non-chunked content
5049 -
5050 - if ($group['is_chunked']) {
5051 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5052 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5053 -
5054 - // Fetch chunks for this URL with limit
5055 - $full_text = $this->reassemble_chunks_from_wordpress($source_url, $chunks_remaining, $chunks_in_this_source);
5056 -
5057 - // If fetching all chunks fails, fall back to matched chunks
5058 - if (empty($full_text)) {
5059 - // Sort matched chunks by index and concatenate
5060 - usort($group['chunks'], function($a, $b) {
5061 - return $a['chunk_index'] <=> $b['chunk_index'];
5062 - });
5063 -
5064 - $chunk_texts = array();
5065 - $chunks_in_this_source = 0;
5066 - foreach ($group['chunks'] as $chunk) {
5067 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5068 - break;
5069 - }
5070 - $chunk_texts[] = $chunk['text'];
5071 - $chunks_in_this_source++;
3720 +
3721 + $chunk_content = $this->fetch_content_with_product_links($result['id']);
3722 + $added_document_ids[] = $result['id'];
3723 +
3724 + $content .= "## Reference " . ($index + 1) . " ##\n";
3725 + $content .= $chunk_content . "\n\n";
3726 +
3727 + // PDF surrounding pages logic (unchanged)
3728 + if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3729 + $surrounding_content = $wpdb->get_results($wpdb->prepare(
3730 + "SELECT id, article_content, role_restriction FROM {$system_prompt_table}
3731 + WHERE id IN (
3732 + (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3733 + (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3734 + )",
3735 + $result['id'],
3736 + $result['id']
3737 + ));
3738 +
3739 + // Check role access for surrounding content too
3740 + if (!empty($surrounding_content[0])) {
3741 + $surrounding_role = $surrounding_content[0]->role_restriction ?? 'public';
3742 + if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
3743 + $content .= "## Related Content ##\n";
3744 + $content .= $surrounding_content[0]->article_content . "\n\n";
3745 + $added_document_ids[] = $surrounding_content[0]->id;
5072 3746 }
5073 - $full_text = implode("\n\n", $chunk_texts);
5074 3747 }
5075 - } else {
5076 - $full_text = $group['single_text'];
5077 - $chunks_in_this_source = 1;
5078 - }
5079 -
5080 - if (!empty($full_text)) {
5081 - // Strip URLs from content if citation links are disabled
5082 - if (!$citation_links_enabled) {
5083 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5084 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
5085 - }
5086 -
5087 - // Use numbered reference for URL-based entries, plain info label for manual entries
5088 - // Manual entries are stored with an internal mxchat:// placeholder URL — never expose them as citations
5089 - if (!empty($source_url) && $source_url !== '#' && strpos($source_url, 'mxchat://') !== 0) {
5090 - $matches_used++;
5091 - $content .= "## Reference " . $matches_used . " ##\n";
5092 - $content .= $full_text . "\n\n";
5093 -
5094 - // Only include citation URLs if citation links are enabled
5095 - if ($citation_links_enabled) {
5096 - $valid_urls[] = $source_url;
5097 - $content .= "URL: " . $source_url . "\n\n";
3748 +
3749 + if (!empty($surrounding_content[1])) {
3750 + $surrounding_role = $surrounding_content[1]->role_restriction ?? 'public';
3751 + if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
3752 + $content .= "## Related Content ##\n";
3753 + $content .= $surrounding_content[1]->article_content . "\n\n";
3754 + $added_document_ids[] = $surrounding_content[1]->id;
5098 3755 }
5099 - } else {
5100 - // Manual entry — no reference number, no citation
5101 - $content .= "## Information ##\n";
5102 - $content .= $full_text . "\n\n";
5103 3756 }
5104 -
5105 - // Extract any URLs from the text content itself (only if citation links enabled)
5106 - if ($citation_links_enabled) {
5107 - preg_match_all(
5108 - '#\bhttps?://[^\s<>"\']+#i',
5109 - $full_text,
5110 - $content_urls
5111 - );
5112 - if (!empty($content_urls[0])) {
5113 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5114 - }
5115 - }
5116 -
5117 - $total_chunks_used += $chunks_in_this_source;
5118 3757 }
5119 3758 }
5120 -
5121 - // NEW: Store unique valid URLs for validation
5122 - $this->current_valid_urls = array_unique($valid_urls);
5123 -
5124 - // Store sources and chunks counts for testing/transcript display
5125 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5126 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5127 -
5128 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5129 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5130 -
3759 +
5131 3760 // Add response guidelines
5132 - if (empty($top_urls)) {
3761 + if (empty($top_results)) {
5133 3762 $content = "No reference information was found for this query.\n\n";
5134 3763 } else {
5135 - // Build response guidelines based on citation links setting
5136 3764 $content .= "\n## Response Guidelines ##\n" .
5137 3765 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5138 3766 "Be conversational and friendly, but never mention your knowledge base or training data. " .
5139 3767 "If you don't have specific information or are uncertain about any details, it's always " .
5140 3768 "better to honestly say you don't know rather than making up or guessing at answers. " .
5141 - "When information is incomplete, let them know you are unsure.\n\n";
5142 -
5143 - // Only add hyperlink instructions if citation links are enabled
5144 - if ($citation_links_enabled) {
5145 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5146 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5147 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
5148 - } else {
5149 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5150 - "Simply provide helpful answers based on the reference information without citing sources.";
5151 - }
3769 + "When information is incomplete, let them know you are unsure.";
5152 3770 }
5153 3771
5154 3772 return trim($content);
5155 3773 }
5156 3774
5157 -/**
5158 - * Fetch and reassemble chunks for a URL from WordPress database
5159 - *
5160 - * @param string $source_url The source URL to fetch chunks for
5161 - * @param int $max_chunks Maximum number of chunks to return (0 = unlimited)
5162 - * @param int &$chunk_count Reference to store the actual number of chunks returned
5163 - * @return string Reassembled content from chunks
5164 - */
5165 -private function reassemble_chunks_from_wordpress($source_url, $max_chunks = 0, &$chunk_count = 0) {
5166 - global $wpdb;
5167 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
5168 -
5169 - // Fetch all rows with this source_url
5170 - $rows = $wpdb->get_results($wpdb->prepare(
5171 - "SELECT article_content FROM {$table}
5172 - WHERE source_url = %s
5173 - ORDER BY id ASC",
5174 - $source_url
5175 - ));
5176 -
5177 - if (empty($rows)) {
5178 - $chunk_count = 0;
5179 - return '';
5180 - }
5181 -
5182 - // Parse and sort chunks by index
5183 - $chunks = array();
5184 - foreach ($rows as $row) {
5185 - $parsed = MxChat_Chunker::parse_stored_chunk($row->article_content);
5186 -
5187 - if ($parsed['is_chunked']) {
5188 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
5189 - $chunks[$chunk_index] = $parsed['text'];
5190 - } else {
5191 - // Non-chunked content - just return it
5192 - $chunks[] = $parsed['text'];
5193 - }
5194 - }
5195 -
5196 - // Sort by chunk index
5197 - ksort($chunks);
5198 -
5199 - // Apply chunk limit if specified
5200 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5201 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5202 - }
5203 -
5204 - // Store actual chunk count
5205 - $chunk_count = count($chunks);
5206 -
5207 - // Reassemble content
5208 - return implode("\n\n", $chunks);
5209 -}
5210 -
5211 3775 private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
5212 3776 global $wpdb;
5213 3777
5214 - //error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
5215 - //error_log(" - bot_id: " . $bot_id);
5216 - //error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
5217 - //error_log(" - user_embedding count: " . (is_array($user_embedding) ? count($user_embedding) : 'N/A'));
3778 + error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
3779 + error_log(" - bot_id: " . $bot_id);
3780 + error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
3781 + error_log(" - user_embedding count: " . (is_array($user_embedding) ? count($user_embedding) : 'N/A'));
5218 3782
5219 3783 // Use bot-specific config or fall back to default
5220 3784 if ($bot_config === null) {
5221 3785 $bot_config = $this->get_bot_pinecone_config($bot_id);
@@ -5224,12 +3788,12 @@
5224 3788 $api_key = $bot_config['api_key'] ?? '';
5225 3789 $host = $bot_config['host'] ?? '';
5226 3790 $namespace = $bot_config['namespace'] ?? '';
5227 3791
5228 - //error_log("MXCHAT DEBUG: Pinecone query parameters:");
5229 - //error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
5230 - //error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
5231 - //error_log(" - Namespace: " . (empty($namespace) ? 'EMPTY (will use default)' : $namespace));
3792 + error_log("MXCHAT DEBUG: Pinecone query parameters:");
3793 + error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
3794 + error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
3795 + error_log(" - Namespace: " . (empty($namespace) ? 'EMPTY (will use default)' : $namespace));
5232 3796
5233 3797 // Initialize similarity analysis storage
5234 3798 $this->last_similarity_analysis = [
5235 3799 'knowledge_base_type' => 'Pinecone',
@@ -5239,17 +3803,12 @@
5239 3803 'threshold_used' => 0,
5240 3804 'total_checked' => 0
5241 3805 ];
5242 3806
5243 - // NEW: Initialize valid URLs array
5244 - $valid_urls = [];
5245 -
5246 3807 if (empty($host) || empty($api_key)) {
5247 - //error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
5248 - //error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
5249 - //error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
5250 - // Store empty array for valid URLs since we can't proceed
5251 - $this->current_valid_urls = [];
3808 + error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
3809 + error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
3810 + error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
5252 3811 return '';
5253 3812 }
5254 3813
5255 3814 // Get knowledge manager instance for role checking
@@ -5269,9 +3828,9 @@
5269 3828 $api_endpoint = "https://{$host}/query";
5270 3829
5271 3830 $request_body = array(
5272 3831 'vector' => $user_embedding,
5273 - 'topK' => 50, // Increased for chunked content grouping - need more candidates to find top N unique URLs
3832 + 'topK' => 20, // Request more to get good testing data
5274 3833 'includeMetadata' => true,
5275 3834 'includeValues' => true
5276 3835 );
5277 3836
@@ -5279,11 +3838,11 @@
5279 3838 if (!empty($namespace)) {
5280 3839 $request_body['namespace'] = $namespace;
5281 3840 }
5282 3841
5283 - //error_log("MXCHAT DEBUG: About to call Pinecone API");
5284 - //error_log(" - Endpoint: " . $api_endpoint);
5285 - //error_log(" - Namespace in request: " . (!empty($namespace) ? $namespace : 'NOT SET'));
3842 + error_log("MXCHAT DEBUG: About to call Pinecone API");
3843 + error_log(" - Endpoint: " . $api_endpoint);
3844 + error_log(" - Namespace in request: " . (!empty($namespace) ? $namespace : 'NOT SET'));
5286 3845
5287 3846 $response = wp_remote_post($api_endpoint, array(
5288 3847 'headers' => array(
5289 3848 'Api-Key' => $api_key,
@@ -5294,61 +3853,53 @@
5294 3853 'timeout' => 30
5295 3854 ));
5296 3855
5297 3856 if (is_wp_error($response)) {
5298 - //error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
5299 - // Store empty array for valid URLs
5300 - $this->current_valid_urls = [];
3857 + error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
5301 3858 return '';
5302 3859 }
5303 3860
5304 3861 $response_code = wp_remote_retrieve_response_code($response);
5305 - //error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
3862 + error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
5306 3863
5307 3864 if ($response_code !== 200) {
5308 3865 $response_body = wp_remote_retrieve_body($response);
5309 - //error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
5310 - // Store empty array for valid URLs
5311 - $this->current_valid_urls = [];
3866 + error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
5312 3867 return '';
5313 3868 }
5314 3869
5315 3870 // ADD DETAILED DEBUG SECTION HERE
5316 3871 $response_body = wp_remote_retrieve_body($response);
5317 - //error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
3872 + error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
5318 3873
5319 3874 $results = json_decode($response_body, true);
5320 3875
5321 3876 if (json_last_error() !== JSON_ERROR_NONE) {
5322 - //error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
5323 - //error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
5324 - // Store empty array for valid URLs
5325 - $this->current_valid_urls = [];
3877 + error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
3878 + error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
5326 3879 return '';
5327 3880 }
5328 3881
5329 - //error_log("MXCHAT DEBUG: Pinecone response structure:");
5330 - //error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
5331 - //error_log(" - Has 'namespace' key: " . (isset($results['namespace']) ? 'yes (' . $results['namespace'] . ')' : 'no'));
3882 + error_log("MXCHAT DEBUG: Pinecone response structure:");
3883 + error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
3884 + error_log(" - Has 'namespace' key: " . (isset($results['namespace']) ? 'yes (' . $results['namespace'] . ')' : 'no'));
5332 3885
5333 3886 if (empty($results['matches'])) {
5334 - //error_log("MXCHAT DEBUG: No matches found in Pinecone response");
5335 - //error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
5336 - // Store empty array for valid URLs
5337 - $this->current_valid_urls = [];
3887 + error_log("MXCHAT DEBUG: No matches found in Pinecone response");
3888 + error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
5338 3889 return '';
5339 3890 }
5340 3891
5341 - //error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
3892 + error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
5342 3893
5343 3894 // Log first match details for debugging
5344 3895 if (!empty($results['matches'][0])) {
5345 3896 $first_match = $results['matches'][0];
5346 - //error_log("MXCHAT DEBUG: First match details:");
5347 - //error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
5348 - //error_log(" - Has metadata: " . (isset($first_match['metadata']) ? 'yes' : 'no'));
3897 + error_log("MXCHAT DEBUG: First match details:");
3898 + error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
3899 + error_log(" - Has metadata: " . (isset($first_match['metadata']) ? 'yes' : 'no'));
5349 3900 if (isset($first_match['metadata'])) {
5350 - //error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
3901 + error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
5351 3902 }
5352 3903 }
5353 3904
5354 3905 // Initialize the final content
@@ -5354,182 +3905,45 @@
5354 3905 // Initialize the final content
5355 3906 $content = '';
5356 3907 $matches_used = 0;
5357 3908 $matches_used_for_context = [];
5358 - $total_chunks_used = 0;
5359 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5360 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5361 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5362 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5363 -
5364 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5365 - // Use fresh options to ensure we get the latest setting value
5366 - $fresh_options = get_option('mxchat_options', []);
5367 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5368 -
5369 - // NEW CHUNKING LOGIC: Group results by source_url for chunk reassembly
5370 - $url_groups = array();
5371 -
3909 +
3910 + // Process each match for actual content generation (lazy role checking)
5372 3911 foreach ($results['matches'] as $index => $match) {
5373 3912 // Skip if similarity is below threshold
5374 3913 if ($match['score'] < $similarity_threshold) {
5375 3914 continue;
5376 3915 }
5377 -
5378 - $metadata = $match['metadata'] ?? array();
5379 - $source_url = $metadata['source_url'] ?? '';
5380 - $match_id = $match['id'] ?? '';
5381 -
5382 - // LAZY ROLE CHECK: Only check role for content we're actually considering
5383 - $role_restriction = $this->get_single_vector_role($match_id, $metadata);
5384 - $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
5385 -
5386 - // Skip if user doesn't have access
5387 - if (!$has_access) {
5388 - continue;
5389 - }
5390 -
5391 - // Use a unique key for manual entries without a source URL
5392 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $match_id;
5393 -
5394 - // Group by source URL (or unique key for manual entries)
5395 - if (!isset($url_groups[$group_key])) {
5396 - $url_groups[$group_key] = array(
5397 - 'source_url' => $source_url,
5398 - 'best_score' => 0,
5399 - 'is_chunked' => isset($metadata['is_chunked']) && $metadata['is_chunked'],
5400 - 'chunks' => array(),
5401 - 'single_text' => ''
5402 - );
5403 - }
5404 -
5405 - // Track best score for this group
5406 - if ($match['score'] > $url_groups[$group_key]['best_score']) {
5407 - $url_groups[$group_key]['best_score'] = $match['score'];
5408 - }
5409 -
5410 - // Store chunk info or single text
5411 - if ($url_groups[$group_key]['is_chunked']) {
5412 - $url_groups[$group_key]['chunks'][] = array(
5413 - 'id' => $match_id,
5414 - 'score' => $match['score'],
5415 - 'chunk_index' => $metadata['chunk_index'] ?? 0,
5416 - 'text' => $metadata['text'] ?? ''
5417 - );
5418 - } else {
5419 - // Non-chunked content - just store the text
5420 - $url_groups[$group_key]['single_text'] = $metadata['text'] ?? '';
5421 - $url_groups[$group_key]['single_id'] = $match_id;
5422 - }
5423 - }
5424 -
5425 - // Sort URL groups by best score (highest first)
5426 - uasort($url_groups, function($a, $b) {
5427 - return $b['best_score'] <=> $a['best_score'];
5428 - });
5429 -
5430 - // Get RAG sources limit from options (default 6, min 3, max 10)
5431 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
5432 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
5433 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
5434 -
5435 - // Take top N unique URLs based on user setting
5436 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5437 -
5438 - // Track which match IDs are actually used for context
5439 - foreach ($top_urls as $group) {
5440 - if ($group['is_chunked']) {
5441 - foreach ($group['chunks'] as $chunk) {
5442 - $matches_used_for_context[] = $chunk['id'];
5443 - }
5444 - } elseif (!empty($group['single_id'])) {
5445 - $matches_used_for_context[] = $group['single_id'];
5446 - }
5447 - }
5448 -
5449 - // Build content from top sources
5450 - foreach ($top_urls as $group_key => $group) {
5451 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5452 -
5453 - // Stop if we've hit the total chunk limit
5454 - if ($total_chunks_used >= $max_total_chunks) {
3916 +
3917 + // Limit to top 5 matches above threshold
3918 + if ($matches_used >= 5) {
5455 3919 break;
5456 3920 }
5457 -
5458 - $full_text = '';
5459 - $chunks_in_this_source = 1; // Default for non-chunked content
5460 -
5461 - if ($group['is_chunked']) {
5462 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5463 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5464 -
5465 - // Fetch chunks for this URL with limit
5466 - $full_text = $this->reassemble_chunks_from_pinecone($source_url, $bot_config, $chunks_remaining, $chunks_in_this_source);
5467 -
5468 - // If fetching all chunks fails, fall back to matched chunks
5469 - if (empty($full_text)) {
5470 - // Sort matched chunks by index and concatenate
5471 - usort($group['chunks'], function($a, $b) {
5472 - return $a['chunk_index'] <=> $b['chunk_index'];
5473 - });
5474 -
5475 - $chunk_texts = array();
5476 - $chunks_in_this_source = 0;
5477 - foreach ($group['chunks'] as $chunk) {
5478 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5479 - break;
5480 - }
5481 - $chunk_texts[] = $chunk['text'];
5482 - $chunks_in_this_source++;
5483 - }
5484 - $full_text = implode("\n\n", $chunk_texts);
3921 +
3922 + if (!empty($match['metadata']['text'])) {
3923 + // LAZY ROLE CHECK: Only check role for content we're actually considering
3924 + $match_id = $match['id'] ?? '';
3925 + $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
3926 + $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
3927 +
3928 + // Skip if user doesn't have access
3929 + if (!$has_access) {
3930 + continue;
5485 3931 }
5486 - } else {
5487 - $full_text = $group['single_text'];
5488 - $chunks_in_this_source = 1;
5489 - }
5490 -
5491 - if (!empty($full_text)) {
5492 - // Strip URLs from content if citation links are disabled
5493 - if (!$citation_links_enabled) {
5494 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5495 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
3932 +
3933 + // User has access - add to content
3934 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
3935 + $content .= $match['metadata']['text'] . "\n\n";
3936 +
3937 + if (!empty($match['metadata']['source_url'])) {
3938 + $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
5496 3939 }
5497 -
5498 - // Use numbered reference for URL-based entries, plain info label for manual entries
5499 - // Manual entries are stored with an internal mxchat:// placeholder URL — never expose them as citations
5500 - if (!empty($source_url) && $source_url !== '#' && strpos($source_url, 'mxchat://') !== 0) {
5501 - $matches_used++;
5502 - $content .= "## Reference " . $matches_used . " ##\n";
5503 - $content .= $full_text . "\n\n";
5504 -
5505 - // Only include citation URLs if citation links are enabled
5506 - if ($citation_links_enabled) {
5507 - $valid_urls[] = $source_url;
5508 - $content .= "URL: " . $source_url . "\n\n";
5509 - }
5510 - } else {
5511 - // Manual entry — no reference number, no citation
5512 - $content .= "## Information ##\n";
5513 - $content .= $full_text . "\n\n";
5514 - }
5515 -
5516 - // Extract any URLs from the text content itself (only if citation links enabled)
5517 - if ($citation_links_enabled) {
5518 - preg_match_all(
5519 - '#\bhttps?://[^\s<>"\']+#i',
5520 - $full_text,
5521 - $content_urls
5522 - );
5523 - if (!empty($content_urls[0])) {
5524 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5525 - }
5526 - }
5527 -
5528 - $total_chunks_used += $chunks_in_this_source;
3940 +
3941 + $matches_used_for_context[] = $match['id'] ?? $index;
3942 + $matches_used++;
5529 3943 }
5530 3944 }
5531 -
3945 +
5532 3946 // Process ALL matches for testing data (top 10) - with role checking for testing display
5533 3947 $all_matches = [];
5534 3948 foreach ($results['matches'] as $index => $match) {
5535 3949 if ($index >= 10) break; // Limit to top 10 for testing
@@ -5549,19 +3963,9 @@
5549 3963 $source_display = substr(trim($content_preview), 0, 50) . '...';
5550 3964 }
5551 3965
5552 3966 $match_id_for_display = $match['id'] ?? $index;
5553 -
5554 - // Check for chunk metadata in Pinecone
5555 - $is_chunk = isset($match['metadata']['is_chunked']) && $match['metadata']['is_chunked'];
5556 - $chunk_index = isset($match['metadata']['chunk_index']) ? intval($match['metadata']['chunk_index']) : null;
5557 - $total_chunks = isset($match['metadata']['total_chunks']) ? intval($match['metadata']['total_chunks']) : null;
5558 -
5559 - // Also detect chunk from vector ID pattern: {hash}_chunk_{index}
5560 - if (!$is_chunk && MxChat_Chunker::is_chunk_vector_id($match_id_for_display)) {
5561 - $is_chunk = true;
5562 - }
5563 -
3967 +
5564 3968 $all_matches[] = [
5565 3969 'document_id' => $match_id_for_display,
5566 3970 'similarity' => $match['score'],
5567 3971 'similarity_percentage' => round($match['score'] * 100, 2),
@@ -5570,12 +3974,9 @@
5570 3974 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
5571 3975 'used_for_context' => in_array($match_id_for_display, $matches_used_for_context),
5572 3976 'role_restriction' => $role_restriction,
5573 3977 'has_access' => $has_access,
5574 - 'filtered_out' => !$has_access,
5575 - 'is_chunk' => $is_chunk,
5576 - 'chunk_index' => $chunk_index,
5577 - 'total_chunks' => $total_chunks
3978 + 'filtered_out' => !$has_access
5578 3979 ];
5579 3980 }
5580 3981
5581 3982 // Store for testing panel
@@ -5580,43 +3981,25 @@
5580 3981
5581 3982 // Store for testing panel
5582 3983 $this->last_similarity_analysis['top_matches'] = $all_matches;
5583 3984 $this->last_similarity_analysis['total_checked'] = count($results['matches']);
5584 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5585 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5586 -
5587 - // NEW: Store unique valid URLs for validation
5588 - $this->current_valid_urls = array_unique($valid_urls);
5589 -
5590 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5591 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5592 -
3985 +
5593 3986 // Add response guidelines
5594 3987 if ($matches_used === 0) {
5595 3988 $content = "No reference information was found for this query.\n\n";
5596 3989 } else {
5597 - // Build response guidelines based on citation links setting
5598 3990 $content .= "\n## Response Guidelines ##\n" .
5599 3991 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5600 3992 "Be conversational and friendly, but never mention your knowledge base or training data. " .
5601 3993 "If you don't have specific information or are uncertain about any details, it's always " .
5602 3994 "better to honestly say you don't know rather than making up or guessing at answers. " .
5603 - "When information is incomplete, let them know you are unsure.\n\n";
5604 -
5605 - // Only add hyperlink instructions if citation links are enabled
5606 - if ($citation_links_enabled) {
5607 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5608 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5609 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
5610 - } else {
5611 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5612 - "Simply provide helpful answers based on the reference information without citing sources.";
5613 - }
3995 + "When information is incomplete, let them know you are unsure.";
5614 3996 }
5615 -
3997 +
5616 3998 return trim($content);
5617 3999 }
5618 4000
4001 +
5619 4002 /**
5620 4003 * Get role restriction for a single vector (with caching)
5621 4004 */
5622 4005 private function get_single_vector_role($vector_id, $metadata = array()) {
@@ -5653,518 +4036,12 @@
5653 4036 }
5654 4037
5655 4038 // Cache individual role for 1 hour
5656 4039 wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
5657 -
4040 +
5658 4041 return $role_restriction;
5659 4042 }
5660 4043
5661 -/**
5662 - * Fetch and reassemble all chunks for a URL from Pinecone
5663 - *
5664 - * @param string $source_url The source URL to fetch chunks for
5665 - * @param array $bot_config Bot-specific Pinecone configuration
5666 - * @return string Reassembled content from all chunks
5667 - */
5668 -private function reassemble_chunks_from_pinecone($source_url, $bot_config, $max_chunks = 0, &$chunk_count = 0) {
5669 - $api_key = $bot_config['api_key'] ?? '';
5670 - $host = $bot_config['host'] ?? '';
5671 - $namespace = $bot_config['namespace'] ?? '';
5672 -
5673 - if (empty($host) || empty($api_key)) {
5674 - $chunk_count = 0;
5675 - return '';
5676 - }
5677 -
5678 - $base_hash = md5($source_url);
5679 -
5680 - // Use Pinecone list API to find all chunk vectors with this prefix
5681 - $list_url = "https://{$host}/vectors/list";
5682 -
5683 - // Limit to max_chunks if specified, otherwise fetch up to 100
5684 - $fetch_limit = ($max_chunks > 0 && $max_chunks < 100) ? $max_chunks : 100;
5685 -
5686 - $list_body = array(
5687 - 'prefix' => $base_hash . '_chunk_',
5688 - 'limit' => $fetch_limit
5689 - );
5690 -
5691 - if (!empty($namespace)) {
5692 - $list_body['namespace'] = $namespace;
5693 - }
5694 -
5695 - $list_response = wp_remote_post($list_url, array(
5696 - 'headers' => array(
5697 - 'Api-Key' => $api_key,
5698 - 'accept' => 'application/json',
5699 - 'content-type' => 'application/json'
5700 - ),
5701 - 'body' => wp_json_encode($list_body),
5702 - 'timeout' => 30
5703 - ));
5704 -
5705 - if (is_wp_error($list_response)) {
5706 - //error_log('[MXCHAT-CHUNK] List API error: ' . $list_response->get_error_message());
5707 - return '';
5708 - }
5709 -
5710 - $list_data = json_decode(wp_remote_retrieve_body($list_response), true);
5711 -
5712 - if (empty($list_data['vectors'])) {
5713 - //error_log('[MXCHAT-CHUNK] No chunk vectors found for URL: ' . $source_url);
5714 - return '';
5715 - }
5716 -
5717 - // Extract vector IDs
5718 - $vector_ids = array();
5719 - foreach ($list_data['vectors'] as $vector) {
5720 - if (isset($vector['id'])) {
5721 - $vector_ids[] = $vector['id'];
5722 - }
5723 - }
5724 -
5725 - if (empty($vector_ids)) {
5726 - return '';
5727 - }
5728 -
5729 - // Fetch all chunk content
5730 - $fetch_url = "https://{$host}/vectors/fetch";
5731 -
5732 - $fetch_body = array(
5733 - 'ids' => $vector_ids
5734 - );
5735 -
5736 - if (!empty($namespace)) {
5737 - $fetch_body['namespace'] = $namespace;
5738 - }
5739 -
5740 - $fetch_response = wp_remote_post($fetch_url, array(
5741 - 'headers' => array(
5742 - 'Api-Key' => $api_key,
5743 - 'accept' => 'application/json',
5744 - 'content-type' => 'application/json'
5745 - ),
5746 - 'body' => wp_json_encode($fetch_body),
5747 - 'timeout' => 30
5748 - ));
5749 -
5750 - if (is_wp_error($fetch_response)) {
5751 - //error_log('[MXCHAT-CHUNK] Fetch API error: ' . $fetch_response->get_error_message());
5752 - return '';
5753 - }
5754 -
5755 - $fetch_data = json_decode(wp_remote_retrieve_body($fetch_response), true);
5756 -
5757 - if (empty($fetch_data['vectors'])) {
5758 - return '';
5759 - }
5760 -
5761 - // Sort chunks by index and reassemble
5762 - $chunks = array();
5763 - foreach ($fetch_data['vectors'] as $id => $vector) {
5764 - $metadata = $vector['metadata'] ?? array();
5765 - $chunk_index = $metadata['chunk_index'] ?? 0;
5766 - $text = $metadata['text'] ?? '';
5767 -
5768 - // Store chunk with its index
5769 - $chunks[$chunk_index] = $text;
5770 - }
5771 -
5772 - // Sort by chunk index
5773 - ksort($chunks);
5774 -
5775 - // Apply chunk limit if specified
5776 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5777 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5778 - }
5779 -
5780 - // Store actual chunk count
5781 - $chunk_count = count($chunks);
5782 -
5783 - // Reassemble content
5784 - return implode("\n\n", $chunks);
5785 -}
5786 -
5787 -/**
5788 - * Search for relevant content using OpenAI Vector Store (File Search)
5789 - *
5790 - * @param string $user_query The user's query text
5791 - * @param string $bot_id The bot ID
5792 - * @param array $vectorstore_config Vector Store configuration
5793 - * @return string Formatted context string with references
5794 - */
5795 -private function find_relevant_content_openai_vectorstore($user_query, $bot_id = 'default', $vectorstore_config = array()) {
5796 - //error_log("MXCHAT DEBUG: find_relevant_content_openai_vectorstore called");
5797 - //error_log(" - bot_id: " . $bot_id);
5798 - //error_log(" - user_query length: " . strlen($user_query));
5799 -
5800 - // Get OpenAI API key
5801 - $mxchat_options = get_option('mxchat_options', array());
5802 - $api_key = $mxchat_options['api_key'] ?? '';
5803 -
5804 - // Reset vectorstore error tracking
5805 - $this->last_vectorstore_error = null;
5806 -
5807 - if (empty($api_key)) {
5808 - //error_log("MXCHAT DEBUG ERROR: OpenAI API key not configured");
5809 - $this->last_vectorstore_error = 'Vector Store search failed: OpenAI API key is not configured.';
5810 - $this->current_valid_urls = [];
5811 - return '';
5812 - }
5813 -
5814 - // Get Vector Store configuration
5815 - if (empty($vectorstore_config)) {
5816 - $vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
5817 - }
5818 -
5819 - $vectorstore_ids_string = $vectorstore_config['vectorstore_ids'] ?? '';
5820 - $max_results = $vectorstore_config['max_results'] ?? 5;
5821 -
5822 - if (empty($vectorstore_ids_string)) {
5823 - //error_log("MXCHAT DEBUG ERROR: No Vector Store IDs configured");
5824 - $this->last_vectorstore_error = 'Vector Store search failed: No Vector Store IDs are configured for this bot.';
5825 - $this->current_valid_urls = [];
5826 - return '';
5827 - }
5828 -
5829 - // Parse Vector Store IDs
5830 - $vectorstore_ids = array_map('trim', explode(',', $vectorstore_ids_string));
5831 - $vectorstore_ids = array_filter($vectorstore_ids); // Remove empty values
5832 -
5833 - //error_log("MXCHAT DEBUG: Vector Store IDs: " . implode(', ', $vectorstore_ids));
5834 - //error_log("MXCHAT DEBUG: Max results: " . $max_results);
5835 -
5836 - // Initialize similarity analysis storage
5837 - $this->last_similarity_analysis = [
5838 - 'knowledge_base_type' => 'OpenAI Vector Store',
5839 - 'bot_id' => $bot_id,
5840 - 'vectorstore_ids' => $vectorstore_ids,
5841 - 'top_matches' => [],
5842 - 'threshold_used' => 0,
5843 - 'total_checked' => 0
5844 - ];
5845 -
5846 - $valid_urls = [];
5847 -
5848 - // Get the selected model
5849 - $bot_options = $this->get_bot_options($bot_id);
5850 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
5851 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
5852 -
5853 - // Verify it's an OpenAI model
5854 - if (!$this->is_openai_chat_model($selected_model)) {
5855 - //error_log("MXCHAT DEBUG ERROR: Vector Store search requires OpenAI model. Current: " . $selected_model);
5856 - $this->last_vectorstore_error = 'Vector Store search requires an OpenAI model. Current model: ' . $selected_model;
5857 - $this->current_valid_urls = [];
5858 - return '';
5859 - }
5860 -
5861 - // Use OpenAI Responses API with file_search tool
5862 - $request_body = array(
5863 - 'model' => $selected_model,
5864 - 'input' => $user_query,
5865 - 'tools' => array(
5866 - array(
5867 - 'type' => 'file_search',
5868 - 'vector_store_ids' => $vectorstore_ids,
5869 - 'max_num_results' => intval($max_results)
5870 - )
5871 - ),
5872 - 'include' => array('output[*].file_search_call.search_results')
5873 - );
5874 -
5875 - //error_log("MXCHAT VECTORSTORE: ========== REQUEST START ==========");
5876 - //error_log("MXCHAT VECTORSTORE: Model: " . $selected_model);
5877 - //error_log("MXCHAT VECTORSTORE: Query: " . substr($user_query, 0, 200));
5878 - //error_log("MXCHAT VECTORSTORE: Vector Store IDs: " . implode(', ', $vectorstore_ids));
5879 - //error_log("MXCHAT VECTORSTORE: Max Results: " . $max_results);
5880 - //error_log("MXCHAT VECTORSTORE: Request body: " . wp_json_encode($request_body));
5881 -
5882 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
5883 - 'headers' => array(
5884 - 'Authorization' => 'Bearer ' . $api_key,
5885 - 'Content-Type' => 'application/json'
5886 - ),
5887 - 'body' => wp_json_encode($request_body),
5888 - 'timeout' => 60
5889 - ));
5890 -
5891 - if (is_wp_error($response)) {
5892 - //error_log("MXCHAT VECTORSTORE ERROR: WP Error: " . $response->get_error_message());
5893 - $this->last_vectorstore_error = 'Vector Store API request failed: ' . $response->get_error_message();
5894 - $this->current_valid_urls = [];
5895 - return '';
5896 - }
5897 -
5898 - $response_code = wp_remote_retrieve_response_code($response);
5899 - //error_log("MXCHAT VECTORSTORE: Response code: " . $response_code);
5900 -
5901 - $response_body = wp_remote_retrieve_body($response);
5902 - //error_log("MXCHAT VECTORSTORE: Raw response (first 2000 chars): " . substr($response_body, 0, 2000));
5903 -
5904 - if ($response_code !== 200) {
5905 - //error_log("MXCHAT VECTORSTORE ERROR: API error response: " . $response_body);
5906 - $api_error_detail = '';
5907 - $decoded_error = json_decode($response_body, true);
5908 - if (isset($decoded_error['error']['message'])) {
5909 - $api_error_detail = $decoded_error['error']['message'];
5910 - }
5911 - $this->last_vectorstore_error = 'Vector Store API returned HTTP ' . $response_code . ($api_error_detail ? ': ' . $api_error_detail : '');
5912 - $this->current_valid_urls = [];
5913 - return '';
5914 - }
5915 - $result = json_decode($response_body, true);
5916 -
5917 - if (json_last_error() !== JSON_ERROR_NONE) {
5918 - //error_log("MXCHAT VECTORSTORE ERROR: JSON decode error: " . json_last_error_msg());
5919 - $this->last_vectorstore_error = 'Vector Store response could not be parsed: ' . json_last_error_msg();
5920 - $this->current_valid_urls = [];
5921 - return '';
5922 - }
5923 -
5924 - // Debug: Log the structure of the result
5925 - //error_log("MXCHAT VECTORSTORE: Result keys: " . implode(', ', array_keys($result)));
5926 - if (isset($result['output'])) {
5927 - //error_log("MXCHAT VECTORSTORE: Output count: " . count($result['output']));
5928 - foreach ($result['output'] as $idx => $out) {
5929 - //error_log("MXCHAT VECTORSTORE: Output[$idx] type: " . ($out['type'] ?? 'unknown'));
5930 - //error_log("MXCHAT VECTORSTORE: Output[$idx] keys: " . implode(', ', array_keys($out)));
5931 - }
5932 - } else {
5933 - //error_log("MXCHAT VECTORSTORE: No 'output' key in result!");
5934 - }
5935 -
5936 - // Extract file search results from the response
5937 - $content = '';
5938 - $matches_used = 0;
5939 - $all_matches = [];
5940 -
5941 - // The Responses API returns output array with tool results
5942 - if (isset($result['output']) && is_array($result['output'])) {
5943 - foreach ($result['output'] as $output_item) {
5944 - // Look for file_search_call results
5945 - if (isset($output_item['type']) && $output_item['type'] === 'file_search_call') {
5946 - //error_log("MXCHAT VECTORSTORE: Found file_search_call output item");
5947 - //error_log("MXCHAT VECTORSTORE: file_search_call keys: " . implode(', ', array_keys($output_item)));
5948 -
5949 - // Check for search_results in the output item directly
5950 - $search_results = $output_item['search_results'] ?? $output_item['results'] ?? [];
5951 - //error_log("MXCHAT VECTORSTORE: Search results count: " . count($search_results));
5952 -
5953 - if (empty($search_results)) {
5954 - //error_log("MXCHAT VECTORSTORE: No search results found in file_search_call");
5955 - //error_log("MXCHAT VECTORSTORE: file_search_call content: " . wp_json_encode($output_item));
5956 - }
5957 -
5958 - foreach ($search_results as $index => $search_result) {
5959 - $filename = $search_result['filename'] ?? '';
5960 - $score = $search_result['score'] ?? 0;
5961 - $text_content = '';
5962 -
5963 - // Extract text content from the result
5964 - // The text can be directly on the result OR nested under content array
5965 - if (isset($search_result['text']) && !empty($search_result['text'])) {
5966 - // Direct text field (OpenAI's actual format)
5967 - $text_content = $search_result['text'];
5968 - //error_log("MXCHAT VECTORSTORE: Found text directly on result[$index], length: " . strlen($text_content));
5969 - } elseif (isset($search_result['content']) && is_array($search_result['content'])) {
5970 - // Nested content array format
5971 - foreach ($search_result['content'] as $content_item) {
5972 - if (isset($content_item['text'])) {
5973 - $text_content .= $content_item['text'] . "\n";
5974 - }
5975 - }
5976 - //error_log("MXCHAT VECTORSTORE: Found text in content array for result[$index], length: " . strlen($text_content));
5977 - } else {
5978 - //error_log("MXCHAT VECTORSTORE: No text found for result[$index]. Keys: " . implode(', ', array_keys($search_result)));
5979 - }
5980 -
5981 - if (!empty($text_content)) {
5982 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
5983 - $content .= trim($text_content) . "\n\n";
5984 -
5985 - if (!empty($filename)) {
5986 - $content .= "Source: " . $filename . "\n\n";
5987 - }
5988 -
5989 - // Extract URLs from content
5990 - preg_match_all(
5991 - '#\bhttps?://[^\s<>"\']+#i',
5992 - $text_content,
5993 - $content_urls
5994 - );
5995 - if (!empty($content_urls[0])) {
5996 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5997 - }
5998 -
5999 - $matches_used++;
6000 - }
6001 -
6002 - // Store for similarity analysis
6003 - $all_matches[] = [
6004 - 'document_id' => $filename ?: ('result_' . $index),
6005 - 'similarity' => $score,
6006 - 'similarity_percentage' => round($score * 100, 2),
6007 - 'above_threshold' => true,
6008 - 'source_display' => $filename,
6009 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6010 - 'used_for_context' => true,
6011 - 'role_restriction' => 'public',
6012 - 'has_access' => true,
6013 - 'filtered_out' => false
6014 - ];
6015 - }
6016 - }
6017 -
6018 - // Also check for message content with annotations (citations)
6019 - if (isset($output_item['type']) && $output_item['type'] === 'message') {
6020 - if (isset($output_item['content']) && is_array($output_item['content'])) {
6021 - foreach ($output_item['content'] as $content_block) {
6022 - if (isset($content_block['annotations']) && is_array($content_block['annotations'])) {
6023 - foreach ($content_block['annotations'] as $annotation) {
6024 - if (isset($annotation['filename'])) {
6025 - $filename = $annotation['filename'];
6026 - $score = $annotation['score'] ?? 0;
6027 - $text_content = '';
6028 -
6029 - if (isset($annotation['content']) && is_array($annotation['content'])) {
6030 - foreach ($annotation['content'] as $ann_content) {
6031 - if (isset($ann_content['text'])) {
6032 - $text_content .= $ann_content['text'] . "\n";
6033 - }
6034 - }
6035 - }
6036 -
6037 - if (!empty($text_content) && $matches_used < $max_results) {
6038 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
6039 - $content .= trim($text_content) . "\n\n";
6040 - $content .= "Source: " . $filename . "\n\n";
6041 -
6042 - preg_match_all(
6043 - '#\bhttps?://[^\s<>"\']+#i',
6044 - $text_content,
6045 - $content_urls
6046 - );
6047 - if (!empty($content_urls[0])) {
6048 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6049 - }
6050 -
6051 - $matches_used++;
6052 -
6053 - $all_matches[] = [
6054 - 'document_id' => $filename,
6055 - 'similarity' => $score,
6056 - 'similarity_percentage' => round($score * 100, 2),
6057 - 'above_threshold' => true,
6058 - 'source_display' => $filename,
6059 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6060 - 'used_for_context' => true,
6061 - 'role_restriction' => 'public',
6062 - 'has_access' => true,
6063 - 'filtered_out' => false
6064 - ];
6065 - }
6066 - }
6067 - }
6068 - }
6069 - }
6070 - }
6071 - }
6072 - }
6073 - }
6074 -
6075 - // Store for testing panel
6076 - $this->last_similarity_analysis['top_matches'] = $all_matches;
6077 - $this->last_similarity_analysis['total_checked'] = count($all_matches);
6078 -
6079 - // Store unique valid URLs for validation
6080 - $this->current_valid_urls = array_unique($valid_urls);
6081 -
6082 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
6083 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
6084 -
6085 - //error_log("MXCHAT VECTORSTORE: ========== SEARCH COMPLETE ==========");
6086 - //error_log("MXCHAT VECTORSTORE: Matches used: " . $matches_used);
6087 - //error_log("MXCHAT VECTORSTORE: All matches count: " . count($all_matches));
6088 - //error_log("MXCHAT VECTORSTORE: Content length: " . strlen($content));
6089 - if ($matches_used > 0) {
6090 - //error_log("MXCHAT VECTORSTORE: Content preview: " . substr($content, 0, 500));
6091 - }
6092 -
6093 - // Check if citation links are enabled
6094 - $citation_links_enabled = ($mxchat_options['citation_links_toggle'] ?? 'on') === 'on';
6095 -
6096 - // Add response guidelines
6097 - if ($matches_used === 0) {
6098 - //error_log("MXCHAT VECTORSTORE: No matches found - returning empty reference message");
6099 - $content = "No reference information was found for this query.\n\n";
6100 - } else {
6101 - // Build response guidelines based on citation links setting
6102 - $content .= "\n## Response Guidelines ##\n" .
6103 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
6104 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
6105 - "If you don't have specific information or are uncertain about any details, it's always " .
6106 - "better to honestly say you don't know rather than making up or guessing at answers. " .
6107 - "When information is incomplete, let them know you are unsure.\n\n";
6108 -
6109 - // Only add hyperlink instructions if citation links are enabled
6110 - if ($citation_links_enabled) {
6111 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
6112 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
6113 - } else {
6114 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
6115 - "Simply provide helpful answers based on the reference information without citing sources.";
6116 - }
6117 - }
6118 -
6119 - //error_log("MXCHAT DEBUG: Vector Store search complete. Matches used: " . $matches_used);
6120 -
6121 - return trim($content);
6122 -}
6123 -
6124 -/**
6125 - * Check if the given model is an OpenAI chat model
6126 - *
6127 - * @param string $model The model ID
6128 - * @return bool True if it's an OpenAI model
6129 - */
6130 -private function is_openai_chat_model($model) {
6131 - $openai_prefixes = array('gpt-', 'o1-', 'o3-');
6132 - foreach ($openai_prefixes as $prefix) {
6133 - if (strpos($model, $prefix) === 0) {
6134 - return true;
6135 - }
6136 - }
6137 - return false;
6138 -}
6139 -
6140 -/**
6141 - * Get bot-specific Vector Store configuration
6142 - *
6143 - * @param string $bot_id The bot ID
6144 - * @return array Configuration array
6145 - */
6146 -private function get_bot_vectorstore_config($bot_id = 'default') {
6147 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
6148 -
6149 - // Default global settings
6150 - $default_config = array(
6151 - 'use_vectorstore' => ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1',
6152 - 'vectorstore_ids' => $vectorstore_options['mxchat_vectorstore_ids'] ?? '',
6153 - 'max_results' => $vectorstore_options['mxchat_vectorstore_max_results'] ?? 5
6154 - );
6155 -
6156 - // Allow multi-bot plugin to override with bot-specific settings
6157 - $bot_config = apply_filters('mxchat_get_bot_vectorstore_config', $default_config, $bot_id);
6158 -
6159 - // Preserve max_results from global settings if not set in bot config
6160 - if (!isset($bot_config['max_results'])) {
6161 - $bot_config['max_results'] = $default_config['max_results'];
6162 - }
6163 -
6164 - return $bot_config;
6165 -}
6166 -
6167 4044 private function mxchat_find_relevant_products($user_embedding) {
6168 4045 //error_log('MXChat Vector Search: Starting product search...');
6169 4046
6170 4047 // Retrieve the add-on settings from the database
@@ -6185,75 +4062,73 @@
6185 4062 }
6186 4063 private function find_relevant_products_wordpress($user_embedding) {
6187 4064 global $wpdb;
6188 4065 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
4066 + $cache_key = 'mxchat_system_prompt_embeddings';
4067 + $batch_size = 500;
6189 4068
6190 - if (!is_array($user_embedding)) {
6191 - return '';
6192 - }
4069 + // Original WordPress database search logic
4070 + // [Previous implementation remains the same]
4071 + $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
4072 + if ($embeddings === false) {
4073 + $embeddings = [];
4074 + $offset = 0;
6193 4075
6194 - // Streaming top-K pass: scan rows in small batches, keep only the top 3
6195 - // results above the similarity threshold. Peak memory is bounded by
6196 - // $batch_size embedding rows plus a 3-element top list.
6197 - $batch_size = 250;
6198 - $similarity_threshold = 0.85;
6199 - $top_k = 3;
6200 - $top_results = [];
6201 - $offset = 0;
4076 + do {
4077 + $query = $wpdb->prepare(
4078 + "SELECT id, embedding_vector
4079 + FROM {$system_prompt_table}
4080 + LIMIT %d OFFSET %d",
4081 + $batch_size,
4082 + $offset
4083 + );
6202 4084
6203 - do {
6204 - $batch = $wpdb->get_results($wpdb->prepare(
6205 - "SELECT id, embedding_vector
6206 - FROM {$system_prompt_table}
6207 - LIMIT %d OFFSET %d",
6208 - $batch_size,
6209 - $offset
6210 - ));
4085 + $batch = $wpdb->get_results($query);
4086 + if (empty($batch)) {
4087 + break;
4088 + }
6211 4089
6212 - if (empty($batch)) {
6213 - break;
6214 - }
4090 + $embeddings = array_merge($embeddings, $batch);
4091 + $offset += $batch_size;
6215 4092
6216 - foreach ($batch as $row) {
6217 - $database_embedding = $row->embedding_vector
6218 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
6219 - : null;
4093 + unset($batch);
6220 4094
6221 - if (!is_array($database_embedding)) {
6222 - unset($database_embedding);
6223 - continue;
6224 - }
4095 + } while (true);
6225 4096
4097 + if (empty($embeddings)) {
4098 + return '';
4099 + }
4100 + wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
4101 + }
4102 +
4103 + $relevant_results = [];
4104 + foreach ($embeddings as $embedding) {
4105 + $database_embedding = $embedding->embedding_vector
4106 + ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
4107 + : null;
4108 + if (is_array($database_embedding) && is_array($user_embedding)) {
6226 4109 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
6227 - unset($database_embedding);
6228 -
6229 - if ($similarity < $similarity_threshold) {
6230 - continue;
6231 - }
6232 -
6233 - // Insert into bounded top-K (kept sorted descending)
6234 - if (count($top_results) < $top_k) {
6235 - $top_results[] = ['id' => $row->id, 'similarity' => $similarity];
6236 - usort($top_results, function ($a, $b) {
6237 - return $b['similarity'] <=> $a['similarity'];
6238 - });
6239 - } elseif ($similarity > $top_results[$top_k - 1]['similarity']) {
6240 - $top_results[$top_k - 1] = ['id' => $row->id, 'similarity' => $similarity];
6241 - usort($top_results, function ($a, $b) {
6242 - return $b['similarity'] <=> $a['similarity'];
6243 - });
6244 - }
4110 + $relevant_results[] = [
4111 + 'id' => $embedding->id,
4112 + 'similarity' => $similarity
4113 + ];
6245 4114 }
4115 + unset($database_embedding);
4116 + }
6246 4117
6247 - unset($batch);
6248 - $offset += $batch_size;
6249 - } while (true);
4118 + // Use fixed threshold for products
4119 + $similarity_threshold = 0.85;
6250 4120
6251 - if (empty($top_results)) {
6252 - return '';
6253 - }
4121 + $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
4122 + return $result['similarity'] >= $similarity_threshold;
4123 + });
4124 + usort($relevant_results, function ($a, $b) {
4125 + return $b['similarity'] <=> $a['similarity'];
4126 + });
6254 4127
4128 + $top_results = array_slice($relevant_results, 0, 5);
6255 4129 $content = '';
4130 +
6256 4131 foreach ($top_results as $result) {
6257 4132 $chunk_content = $this->fetch_content_with_product_links($result['id']);
6258 4133 $content .= $chunk_content . "\n\n";
6259 4134 }
@@ -6362,61 +4237,23 @@
6362 4237
6363 4238 /**
6364 4239 * Get system instructions for a specific bot or default
6365 4240 * Checks for multi-bot add-on and uses bot-specific instructions if available
6366 - * Automatically strips URLs if citation links are disabled
6367 - * Replaces {visitor_name} placeholder with actual visitor name if available
6368 - *
6369 - * @param string $bot_id The bot ID to get instructions for
6370 - * @param string $session_id Optional session ID to lookup visitor name
6371 4241 */
6372 -private function get_system_instructions($bot_id = 'default', $session_id = '') {
6373 - $instructions = '';
6374 -
4242 +private function get_system_instructions($bot_id = 'default') {
6375 4243 // Check if multi-bot add-on is active
6376 4244 if (class_exists('MxChat_Multi_Bot_Core_Manager') && $bot_id !== 'default') {
6377 4245 // Get bot-specific options from multi-bot add-on
6378 4246 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
6379 -
4247 +
6380 4248 // If bot has custom system instructions, use those
6381 4249 if (!empty($bot_options['system_prompt_instructions'])) {
6382 - $instructions = $bot_options['system_prompt_instructions'];
4250 + return $bot_options['system_prompt_instructions'];
6383 4251 }
6384 4252 }
6385 -
4253 +
6386 4254 // Fall back to default system instructions
6387 - if (empty($instructions)) {
6388 - $instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6389 - }
6390 -
6391 - // Check if citation links are disabled - if so, strip URLs from instructions
6392 - $fresh_options = get_option('mxchat_options', []);
6393 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
6394 -
6395 - if (!$citation_links_enabled && !empty($instructions)) {
6396 - $instructions = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $instructions);
6397 - $instructions = preg_replace('/\s+/', ' ', trim($instructions)); // Clean up extra spaces
6398 - }
6399 -
6400 - // Replace {visitor_name} placeholder with actual visitor name if available
6401 - if (!empty($instructions) && !empty($session_id) && stripos($instructions, '{visitor_name}') !== false) {
6402 - $name_option_key = "mxchat_name_{$session_id}";
6403 - $visitor_name = get_option($name_option_key, '');
6404 -
6405 - if (!empty($visitor_name)) {
6406 - $instructions = str_ireplace('{visitor_name}', sanitize_text_field($visitor_name), $instructions);
6407 - } else {
6408 - // Remove placeholder if no name is available
6409 - $instructions = str_ireplace('{visitor_name}', '', $instructions);
6410 - $instructions = preg_replace('/\s{2,}/', ' ', trim($instructions)); // Clean up extra spaces
6411 - }
6412 - }
6413 -
6414 - // Allow developers to filter system instructions and process shortcodes
6415 - $instructions = apply_filters('mxchat_system_instructions', $instructions, $bot_id, $session_id);
6416 - $instructions = do_shortcode($instructions);
6417 -
6418 - return $instructions;
4255 + return isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6419 4256 }
6420 4257 /**
6421 4258 * Get the current bot ID from session or request context
6422 4259 */
@@ -6436,9 +4273,9 @@
6436 4273
6437 4274 // Fall back to default
6438 4275 return 'default';
6439 4276 }
6440 -private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $gemini_api_key, $openrouter_api_key, $conversation_history, $streaming = false, $session_id = '', $testing_data = null, $selected_model = 'gpt-5.1-chat-latest') {
4277 +private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $gemini_api_key, $conversation_history, $streaming = false, $session_id = '', $testing_data = null, $selected_model = 'gpt-4o') {
6441 4278 try {
6442 4279 if (!$relevant_content) {
6443 4280 $error_response = [
6444 4281 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
@@ -6444,73 +4281,22 @@
6444 4281 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
6445 4282 'error_code' => 'no_relevant_content'
6446 4283 ];
6447 4284
4285 + // Add testing data to error response if available
6448 4286 if ($testing_data !== null) {
6449 4287 $error_response['testing_data'] = $testing_data;
4288 + //error_log("MxChat Testing: Added testing data to no_relevant_content error");
6450 4289 }
6451 4290
6452 4291 return $error_response;
6453 4292 }
6454 4293
4294 + // Ensure conversation_history is an array
6455 4295 if (!is_array($conversation_history)) {
6456 4296 $conversation_history = array();
6457 4297 }
6458 4298
6459 - // Check if this is an OpenRouter model
6460 - if ($selected_model === 'openrouter') {
6461 - // Get the actual OpenRouter model from options
6462 - $openrouter_selected_model = $this->options['openrouter_selected_model'] ?? '';
6463 -
6464 - if (empty($openrouter_selected_model)) {
6465 - $error_response = [
6466 - 'error' => esc_html__('No OpenRouter model selected. Please select a model in settings.', 'mxchat'),
6467 - 'error_code' => 'no_openrouter_model_selected'
6468 - ];
6469 - if ($testing_data !== null) {
6470 - $error_response['testing_data'] = $testing_data;
6471 - }
6472 - return $error_response;
6473 - }
6474 -
6475 - if (empty($openrouter_api_key)) {
6476 - $error_response = [
6477 - 'error' => esc_html__('OpenRouter API key is not configured', 'mxchat'),
6478 - 'error_code' => 'missing_openrouter_api_key'
6479 - ];
6480 - if ($testing_data !== null) {
6481 - $error_response['testing_data'] = $testing_data;
6482 - }
6483 - return $error_response;
6484 - }
6485 -
6486 - if ($streaming) {
6487 - return $this->mxchat_generate_response_openrouter_stream(
6488 - $openrouter_selected_model,
6489 - $openrouter_api_key,
6490 - $conversation_history,
6491 - $relevant_content,
6492 - $session_id,
6493 - $testing_data
6494 - );
6495 - } else {
6496 - $response = $this->mxchat_generate_response_openrouter(
6497 - $openrouter_selected_model,
6498 - $openrouter_api_key,
6499 - $conversation_history,
6500 - $relevant_content
6501 - );
6502 - }
6503 -
6504 - if (is_array($response) && isset($response['error'])) {
6505 - if ($testing_data !== null) {
6506 - $response['testing_data'] = $testing_data;
6507 - }
6508 - return $response;
6509 - }
6510 -
6511 - return $response;
6512 - }
6513 4299
6514 4300 // Extract model prefix to determine the provider
6515 4301 $model_parts = explode('-', $selected_model);
6516 4302 $provider = strtolower($model_parts[0]);
@@ -6553,9 +4339,9 @@
6553 4339 $claude_api_key,
6554 4340 $conversation_history,
6555 4341 $relevant_content,
6556 4342 $session_id,
6557 - $testing_data
4343 + $testing_data // Pass testing data
6558 4344 );
6559 4345 } else {
6560 4346 $response = $this->mxchat_generate_response_claude(
6561 4347 $selected_model,
@@ -6583,9 +4369,9 @@
6583 4369 $xai_api_key,
6584 4370 $conversation_history,
6585 4371 $relevant_content,
6586 4372 $session_id,
6587 - $testing_data
4373 + $testing_data // Pass testing data
6588 4374 );
6589 4375 } else {
6590 4376 $response = $this->mxchat_generate_response_xai(
6591 4377 $selected_model,
@@ -6613,9 +4399,9 @@
6613 4399 $deepseek_api_key,
6614 4400 $conversation_history,
6615 4401 $relevant_content,
6616 4402 $session_id,
6617 - $testing_data
4403 + $testing_data // Pass testing data
6618 4404 );
6619 4405 } else {
6620 4406 $response = $this->mxchat_generate_response_deepseek(
6621 4407 $selected_model,
@@ -6637,27 +4423,9 @@
6637 4423 $error_response['testing_data'] = $testing_data;
6638 4424 }
6639 4425 return $error_response;
6640 4426 }
6641 -
6642 - // Check if web search is enabled for this OpenAI model
6643 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6644 - // Models that don't support web search
6645 - $unsupported_web_search_models = array('gpt-4.1-nano');
6646 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6647 -
6648 - if ($web_search_enabled && $model_supports_web_search) {
6649 - // Use Responses API (required for some models, or when web search is enabled)
6650 - return $this->mxchat_generate_response_openai_web_search(
6651 - $selected_model,
6652 - $api_key,
6653 - $conversation_history,
6654 - $relevant_content,
6655 - $session_id,
6656 - $testing_data,
6657 - $streaming
6658 - );
6659 - } elseif ($streaming) {
4427 + if ($streaming) {
6660 4428 return $this->mxchat_generate_response_openai_stream(
6661 4429 $selected_model,
6662 4430 $api_key,
6663 4431 $conversation_history,
@@ -6662,9 +4430,9 @@
6662 4430 $api_key,
6663 4431 $conversation_history,
6664 4432 $relevant_content,
6665 4433 $session_id,
6666 - $testing_data
4434 + $testing_data // Pass testing data
6667 4435 );
6668 4436 } else {
6669 4437 $response = $this->mxchat_generate_response_openai(
6670 4438 $selected_model,
@@ -6675,8 +4443,9 @@
6675 4443 }
6676 4444 break;
6677 4445
6678 4446 default:
4447 + // Default to OpenAI for custom models or unrecognized prefixes
6679 4448 if (empty($api_key)) {
6680 4449 $error_response = [
6681 4450 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
6682 4451 'error_code' => 'missing_openai_api_key'
@@ -6685,25 +4454,9 @@
6685 4454 $error_response['testing_data'] = $testing_data;
6686 4455 }
6687 4456 return $error_response;
6688 4457 }
6689 -
6690 - // Check if web search is enabled (default case also handles OpenAI models)
6691 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6692 - $unsupported_web_search_models = array('gpt-4.1-nano');
6693 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6694 -
6695 - if ($web_search_enabled && $model_supports_web_search) {
6696 - return $this->mxchat_generate_response_openai_web_search(
6697 - $selected_model,
6698 - $api_key,
6699 - $conversation_history,
6700 - $relevant_content,
6701 - $session_id,
6702 - $testing_data,
6703 - $streaming
6704 - );
6705 - } elseif ($streaming) {
4458 + if ($streaming) {
6706 4459 return $this->mxchat_generate_response_openai_stream(
6707 4460 $selected_model,
6708 4461 $api_key,
6709 4462 $conversation_history,
@@ -6708,9 +4461,9 @@
6708 4461 $api_key,
6709 4462 $conversation_history,
6710 4463 $relevant_content,
6711 4464 $session_id,
6712 - $testing_data
4465 + $testing_data // Pass testing data
6713 4466 );
6714 4467 } else {
6715 4468 $response = $this->mxchat_generate_response_openai(
6716 4469 $selected_model,
@@ -6721,18 +4474,24 @@
6721 4474 }
6722 4475 break;
6723 4476 }
6724 4477
4478 + // Check if the response is an error array from the provider-specific function
6725 4479 if (is_array($response) && isset($response['error'])) {
4480 + // Add testing data to error response if available
6726 4481 if ($testing_data !== null) {
6727 4482 $response['testing_data'] = $testing_data;
4483 + //error_log("MxChat Testing: Added testing data to provider error response");
6728 4484 }
6729 - return $response;
4485 + return $response; // Pass through the error with testing data
6730 4486 }
6731 4487
4488 + // For successful non-streaming responses, we don't add testing data here
4489 + // because it will be added in the main handler
6732 4490 return $response;
6733 4491
6734 4492 } catch (Exception $e) {
4493 + //error_log('MXChat Error: ' . $e->getMessage());
6735 4494 $error_response = [
6736 4495 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
6737 4496 'error_code' => 'system_exception',
6738 4497 'exception_details' => $e->getMessage()
@@ -6737,229 +4496,24 @@
6737 4496 'error_code' => 'system_exception',
6738 4497 'exception_details' => $e->getMessage()
6739 4498 ];
6740 4499
4500 + // Add testing data to exception response if available
6741 4501 if ($testing_data !== null) {
6742 4502 $error_response['testing_data'] = $testing_data;
4503 + //error_log("MxChat Testing: Added testing data to exception response");
6743 4504 }
6744 4505
6745 4506 return $error_response;
6746 4507 }
6747 4508 }
6748 -private function mxchat_generate_response_openrouter_stream($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
6749 - try {
6750 - $bot_id = $this->get_current_bot_id($session_id);
6751 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6752 -
6753 - if (!is_array($conversation_history)) {
6754 - $conversation_history = array();
6755 - }
6756 4509
6757 - $formatted_conversation = array();
6758 -
6759 - $formatted_conversation[] = array(
6760 - 'role' => 'system',
6761 - 'content' => $system_prompt_instructions . " " . $relevant_content
6762 - );
6763 -
6764 - foreach ($conversation_history as $message) {
6765 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6766 - $role = $message['role'];
6767 - if ($role === 'bot' || $role === 'agent') {
6768 - $role = 'assistant';
6769 - }
6770 - if (!in_array($role, ['system', 'assistant', 'user'])) {
6771 - $role = 'user';
6772 - }
6773 - $formatted_conversation[] = array(
6774 - 'role' => $role,
6775 - 'content' => $message['content']
6776 - );
6777 - }
6778 - }
6779 -
6780 - if (headers_sent() || !function_exists('curl_init')) {
6781 - $regular_response = $this->mxchat_generate_response_openrouter(
6782 - $selected_model,
6783 - $openrouter_api_key,
6784 - $conversation_history,
6785 - $relevant_content
6786 - );
6787 -
6788 - // Save bot response to transcript
6789 - if (!empty($regular_response) && !empty($session_id)) {
6790 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
6791 - }
6792 -
6793 - $response_data = [
6794 - 'text' => $regular_response,
6795 - 'html' => '',
6796 - 'session_id' => $session_id
6797 - ];
6798 -
6799 - if ($testing_data !== null) {
6800 - $response_data['testing_data'] = $testing_data;
6801 - }
6802 -
6803 - header('Content-Type: application/json');
6804 - echo json_encode($response_data);
6805 - return true;
6806 - }
6807 -
6808 - $body = json_encode([
6809 - 'model' => $selected_model,
6810 - 'messages' => $formatted_conversation,
6811 - 'temperature' => 1,
6812 - 'stream' => true
6813 - ]);
6814 -
6815 - // Setup streaming headers now that we know we're actually streaming
6816 - $this->setup_streaming_headers();
6817 -
6818 - $ch = curl_init();
6819 - curl_setopt($ch, CURLOPT_URL, 'https://openrouter.ai/api/v1/chat/completions');
6820 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
6821 - curl_setopt($ch, CURLOPT_POST, true);
6822 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
6823 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
6824 - 'Content-Type: application/json',
6825 - 'Authorization: Bearer ' . $openrouter_api_key,
6826 - 'HTTP-Referer: ' . home_url(),
6827 - 'X-Title: ' . get_bloginfo('name')
6828 - ));
6829 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
6830 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
6831 -
6832 - $full_response = '';
6833 - $stream_started = false;
6834 - $buffer = '';
6835 -
6836 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
6837 - if (!$stream_started && $testing_data !== null) {
6838 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
6839 - flush();
6840 - $stream_started = true;
6841 - }
6842 -
6843 - $buffer .= $data;
6844 - $lines = explode("\n", $buffer);
6845 - $buffer = array_pop($lines);
6846 -
6847 - foreach ($lines as $line) {
6848 - if (trim($line) === '') {
6849 - continue;
6850 - }
6851 -
6852 - if (strpos($line, 'data: ') !== 0) {
6853 - continue;
6854 - }
6855 -
6856 - $json_str = substr($line, 6);
6857 -
6858 - if (trim($json_str) === '[DONE]') {
6859 - echo "data: [DONE]\n\n";
6860 - flush();
6861 - continue;
6862 - }
6863 -
6864 - $json = json_decode(trim($json_str), true);
6865 - if ($json && isset($json['choices'][0]['delta']['content'])) {
6866 - $content = $json['choices'][0]['delta']['content'];
6867 - $full_response .= $content;
6868 -
6869 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
6870 - flush();
6871 - }
6872 - }
6873 -
6874 - return strlen($data);
6875 - });
6876 -
6877 - $response = curl_exec($ch);
6878 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
6879 -
6880 - if (curl_errno($ch) || $http_code !== 200) {
6881 - curl_close($ch);
6882 -
6883 - $regular_response = $this->mxchat_generate_response_openrouter(
6884 - $selected_model,
6885 - $openrouter_api_key,
6886 - $conversation_history,
6887 - $relevant_content
6888 - );
6889 -
6890 - $response_data = [
6891 - 'text' => $regular_response,
6892 - 'html' => '',
6893 - 'session_id' => $session_id
6894 - ];
6895 -
6896 - if ($testing_data !== null) {
6897 - $response_data['testing_data'] = $testing_data;
6898 - }
6899 -
6900 - header('Content-Type: application/json');
6901 - echo json_encode($response_data);
6902 - return true;
6903 - }
6904 -
6905 - curl_close($ch);
6906 -
6907 - if (!empty($full_response) && !empty($session_id)) {
6908 - // Prepare RAG context for streaming response
6909 - $rag_context_for_storage = null;
6910 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
6911 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
6912 -
6913 - if ($has_rag_data || $has_action_data) {
6914 - $rag_context_for_storage = [];
6915 -
6916 - if ($has_rag_data) {
6917 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
6918 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
6919 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
6920 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
6921 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
6922 - }
6923 -
6924 - if ($has_action_data) {
6925 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
6926 - }
6927 - }
6928 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
6929 - }
6930 -
6931 - return true;
6932 -
6933 - } catch (Exception $e) {
6934 - $regular_response = $this->mxchat_generate_response_openrouter(
6935 - $selected_model,
6936 - $openrouter_api_key,
6937 - $conversation_history,
6938 - $relevant_content
6939 - );
6940 -
6941 - $response_data = [
6942 - 'text' => $regular_response,
6943 - 'html' => '',
6944 - 'session_id' => $session_id
6945 - ];
6946 -
6947 - if ($testing_data !== null) {
6948 - $response_data['testing_data'] = $testing_data;
6949 - }
6950 -
6951 - header('Content-Type: application/json');
6952 - echo json_encode($response_data);
6953 - return true;
6954 - }
6955 -}
6956 4510 private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
6957 4511 try {
6958 4512 $bot_id = $this->get_current_bot_id($session_id);
6959 4513
6960 4514 // Get system prompt instructions using centralized function
6961 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
4515 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
6962 4516
6963 4517 // Ensure conversation_history is an array
6964 4518 if (!is_array($conversation_history)) {
6965 4519 $conversation_history = array();
@@ -6998,13 +4552,8 @@
6998 4552 $conversation_history,
6999 4553 $relevant_content
7000 4554 );
7001 4555
7002 - // Save bot response to transcript
7003 - if (!empty($regular_response) && !empty($session_id)) {
7004 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7005 - }
7006 -
7007 4556 $response_data = [
7008 4557 'text' => $regular_response,
7009 4558 'html' => '',
7010 4559 'session_id' => $session_id
@@ -7018,45 +4567,16 @@
7018 4567 echo json_encode($response_data);
7019 4568 return true;
7020 4569 }
7021 4570
7022 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
7023 - $is_gpt5_model = (
7024 - strpos($selected_model, 'gpt-5') === 0 ||
7025 - $selected_model === 'gpt-5.2' ||
7026 - $selected_model === 'gpt-5.1-2025-11-13' ||
7027 - $selected_model === 'gpt-5' ||
7028 - $selected_model === 'gpt-5-mini' ||
7029 - $selected_model === 'gpt-5-nano'
7030 - );
7031 -
7032 - // Build request body with optimal settings for fast streaming
7033 - $request_body = [
4571 + // Prepare the request body with stream: true
4572 + $body = json_encode([
7034 4573 'model' => $selected_model,
7035 4574 'messages' => $formatted_conversation,
7036 4575 'temperature' => 1,
7037 4576 'stream' => true
7038 - ];
4577 + ]);
7039 4578
7040 - // Add reasoning_effort only for GPT-5 models that support it
7041 - // These chat models don't support reasoning_effort parameter
7042 - $no_reasoning_models = array('gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
7043 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
7044 - // GPT-5.1 uses 'low' instead of 'minimal'
7045 - if ($selected_model === 'gpt-5.1-2025-11-13') {
7046 - $request_body['reasoning_effort'] = 'low';
7047 - } elseif ($selected_model === 'gpt-5.4') {
7048 - $request_body['reasoning_effort'] = 'none';
7049 - } else {
7050 - $request_body['reasoning_effort'] = 'minimal';
7051 - }
7052 - }
7053 -
7054 - $body = json_encode($request_body);
7055 -
7056 - // Setup streaming headers now that we know we're actually streaming
7057 - $this->setup_streaming_headers();
7058 -
7059 4579 // Use cURL for streaming support
7060 4580 $ch = curl_init();
7061 4581 curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
7062 4582 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -7129,11 +4649,10 @@
7129 4649 $response = curl_exec($ch);
7130 4650 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7131 4651
7132 4652 if (curl_errno($ch) || $http_code !== 200) {
7133 - $curl_error = curl_error($ch);
7134 4653 curl_close($ch);
7135 -
4654 +
7136 4655 // Fallback to regular response
7137 4656 $regular_response = $this->mxchat_generate_response_openai(
7138 4657 $selected_model,
7139 4658 $api_key,
@@ -7139,34 +4658,19 @@
7139 4658 $api_key,
7140 4659 $conversation_history,
7141 4660 $relevant_content
7142 4661 );
7143 -
7144 - // FIXED: Check if regular response returned an error
7145 - if (is_array($regular_response) && isset($regular_response['error'])) {
7146 - // Send error in SSE format since we're in streaming mode
7147 - echo "data: " . json_encode([
7148 - 'error' => true,
7149 - 'error_message' => $regular_response['error'],
7150 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7151 - 'text' => $regular_response['error'],
7152 - 'message' => $regular_response['error']
7153 - ]) . "\n\n";
7154 - echo "data: [DONE]\n\n";
7155 - flush();
7156 - return true;
7157 - }
7158 -
4662 +
7159 4663 $response_data = [
7160 4664 'text' => $regular_response,
7161 4665 'html' => '',
7162 4666 'session_id' => $session_id
7163 4667 ];
7164 -
4668 +
7165 4669 if ($testing_data !== null) {
7166 4670 $response_data['testing_data'] = $testing_data;
7167 4671 }
7168 -
4672 +
7169 4673 header('Content-Type: application/json');
7170 4674 echo json_encode($response_data);
7171 4675 return true;
7172 4676 }
@@ -7171,36 +4675,16 @@
7171 4675 return true;
7172 4676 }
7173 4677
7174 4678 curl_close($ch);
7175 -
4679 +
7176 4680 // Save the complete response to maintain chat persistence
7177 4681 if (!empty($full_response) && !empty($session_id)) {
7178 - // Prepare RAG context for streaming response
7179 - $rag_context_for_storage = null;
7180 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7181 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7182 -
7183 - if ($has_rag_data || $has_action_data) {
7184 - $rag_context_for_storage = [];
7185 -
7186 - if ($has_rag_data) {
7187 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7188 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7189 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7190 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7191 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7192 - }
7193 -
7194 - if ($has_action_data) {
7195 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7196 - }
7197 - }
7198 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
4682 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
7199 4683 }
7200 -
4684 +
7201 4685 return true; // Indicate streaming completed successfully
7202 -
4686 +
7203 4687 } catch (Exception $e) {
7204 4688 // Fallback to regular response
7205 4689 $regular_response = $this->mxchat_generate_response_openai(
7206 4690 $selected_model,
@@ -7207,394 +4691,24 @@
7207 4691 $api_key,
7208 4692 $conversation_history,
7209 4693 $relevant_content
7210 4694 );
7211 -
7212 - // FIXED: Check if regular response returned an error
7213 - if (is_array($regular_response) && isset($regular_response['error'])) {
7214 - // Send error in SSE format since we're in streaming mode
7215 - echo "data: " . json_encode([
7216 - 'error' => true,
7217 - 'error_message' => $regular_response['error'],
7218 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7219 - 'text' => $regular_response['error'],
7220 - 'message' => $regular_response['error']
7221 - ]) . "\n\n";
7222 - echo "data: [DONE]\n\n";
7223 - flush();
7224 - return true;
7225 - }
7226 -
4695 +
7227 4696 $response_data = [
7228 4697 'text' => $regular_response,
7229 4698 'html' => '',
7230 4699 'session_id' => $session_id
7231 4700 ];
7232 -
4701 +
7233 4702 if ($testing_data !== null) {
7234 4703 $response_data['testing_data'] = $testing_data;
7235 4704 }
7236 -
4705 +
7237 4706 header('Content-Type: application/json');
7238 4707 echo json_encode($response_data);
7239 4708 return true;
7240 4709 }
7241 4710 }
7242 -
7243 -/**
7244 - * Generate response using OpenAI Responses API with web search tool
7245 - * This uses the newer Responses API which supports web search functionality
7246 - */
7247 -private function mxchat_generate_response_openai_web_search($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null, $streaming = false) {
7248 - try {
7249 - $bot_id = $this->get_current_bot_id($session_id);
7250 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7251 -
7252 - if (!is_array($conversation_history)) {
7253 - $conversation_history = array();
7254 - }
7255 -
7256 - // Build the input for Responses API
7257 - // The Responses API uses a different format - we need to construct the input properly
7258 - $input_parts = [];
7259 -
7260 - // Add system instructions as context
7261 - $system_context = $system_prompt_instructions . "\n\n" . $relevant_content;
7262 -
7263 - // Build conversation as input items for Responses API
7264 - foreach ($conversation_history as $message) {
7265 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7266 - $role = $message['role'];
7267 - if ($role === 'bot' || $role === 'agent') {
7268 - $role = 'assistant';
7269 - }
7270 - if (!in_array($role, ['assistant', 'user'])) {
7271 - $role = 'user';
7272 - }
7273 - $input_parts[] = [
7274 - 'type' => 'message',
7275 - 'role' => $role,
7276 - 'content' => $message['content']
7277 - ];
7278 - }
7279 - }
7280 -
7281 - // Build request body for Responses API
7282 - $request_body = [
7283 - 'model' => $selected_model,
7284 - 'input' => $input_parts,
7285 - 'instructions' => $system_context,
7286 - 'stream' => $streaming
7287 - ];
7288 -
7289 - // Only add web search tool if web search is enabled in settings
7290 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
7291 - if ($web_search_enabled) {
7292 - $request_body['tools'] = [
7293 - ['type' => 'web_search']
7294 - ];
7295 - }
7296 -
7297 - // Add reasoning effort for supported models
7298 - $is_gpt5_model = strpos($selected_model, 'gpt-5') === 0;
7299 - $no_reasoning_web = array('gpt-5.2', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
7300 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_web, true)) {
7301 - if ($selected_model === 'gpt-5.1-2025-11-13') {
7302 - $request_body['reasoning'] = ['effort' => 'low'];
7303 - } elseif ($selected_model === 'gpt-5.4') {
7304 - $request_body['reasoning'] = ['effort' => 'low'];
7305 - }
7306 - }
7307 -
7308 - //error_log("MXCHAT WEB SEARCH: Request body: " . json_encode($request_body));
7309 -
7310 - if ($streaming) {
7311 - return $this->mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data);
7312 - } else {
7313 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7314 - }
7315 -
7316 - } catch (Exception $e) {
7317 - //error_log("MXCHAT WEB SEARCH ERROR: " . $e->getMessage());
7318 - return [
7319 - 'error' => sprintf(esc_html__('Web search error: %s', 'mxchat'), esc_html($e->getMessage())),
7320 - 'error_code' => 'web_search_exception'
7321 - ];
7322 - }
7323 -}
7324 -
7325 -/**
7326 - * Handle non-streaming web search response
7327 - */
7328 -private function mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7329 - $request_body['stream'] = false;
7330 -
7331 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
7332 - 'headers' => array(
7333 - 'Authorization' => 'Bearer ' . $api_key,
7334 - 'Content-Type' => 'application/json'
7335 - ),
7336 - 'body' => json_encode($request_body),
7337 - 'timeout' => 90
7338 - ));
7339 -
7340 - if (is_wp_error($response)) {
7341 - //error_log("MXCHAT WEB SEARCH ERROR: WP Error: " . $response->get_error_message());
7342 - return [
7343 - 'error' => esc_html__('Failed to connect to OpenAI web search API', 'mxchat'),
7344 - 'error_code' => 'web_search_connection_error'
7345 - ];
7346 - }
7347 -
7348 - $response_code = wp_remote_retrieve_response_code($response);
7349 - $response_body = wp_remote_retrieve_body($response);
7350 -
7351 - //error_log("MXCHAT WEB SEARCH: Response code: " . $response_code);
7352 - //error_log("MXCHAT WEB SEARCH: Response body (first 2000): " . substr($response_body, 0, 2000));
7353 -
7354 - if ($response_code !== 200) {
7355 - $error_data = json_decode($response_body, true);
7356 - $error_message = $error_data['error']['message'] ?? 'Unknown API error';
7357 - return [
7358 - 'error' => sprintf(esc_html__('OpenAI API error: %s', 'mxchat'), esc_html($error_message)),
7359 - 'error_code' => 'web_search_api_error'
7360 - ];
7361 - }
7362 -
7363 - $result = json_decode($response_body, true);
7364 -
7365 - if (json_last_error() !== JSON_ERROR_NONE) {
7366 - return [
7367 - 'error' => esc_html__('Invalid response from OpenAI', 'mxchat'),
7368 - 'error_code' => 'web_search_json_error'
7369 - ];
7370 - }
7371 -
7372 - // Extract the response text and citations from Responses API format
7373 - $output_text = '';
7374 - $citations = [];
7375 -
7376 - if (isset($result['output'])) {
7377 - foreach ($result['output'] as $output_item) {
7378 - if ($output_item['type'] === 'message' && isset($output_item['content'])) {
7379 - foreach ($output_item['content'] as $content_item) {
7380 - if ($content_item['type'] === 'output_text') {
7381 - $output_text .= $content_item['text'];
7382 -
7383 - // Extract citations/annotations
7384 - if (isset($content_item['annotations'])) {
7385 - foreach ($content_item['annotations'] as $annotation) {
7386 - if ($annotation['type'] === 'url_citation') {
7387 - $citations[] = [
7388 - 'url' => $annotation['url'],
7389 - 'title' => $annotation['title'] ?? ''
7390 - ];
7391 - }
7392 - }
7393 - }
7394 - }
7395 - }
7396 - }
7397 - }
7398 - }
7399 -
7400 - // If we have citations, append them to the response
7401 - if (!empty($citations)) {
7402 - $output_text .= "\n\n**Sources:**\n";
7403 - $seen_urls = [];
7404 - foreach ($citations as $citation) {
7405 - if (!in_array($citation['url'], $seen_urls)) {
7406 - $seen_urls[] = $citation['url'];
7407 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7408 - $output_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7409 - }
7410 - }
7411 - }
7412 -
7413 - // Transcript save is handled by the main handler (mxchat_handle_chat_request)
7414 - // which includes rag_context for the "sources" link in transcripts.
7415 -
7416 - return $output_text;
7417 -}
7418 -
7419 -/**
7420 - * Handle streaming web search response using Responses API
7421 - */
7422 -private function mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7423 - $request_body['stream'] = true;
7424 -
7425 - // Check if we can stream
7426 - if (headers_sent() || !function_exists('curl_init')) {
7427 - // Fallback to non-streaming
7428 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7429 - }
7430 -
7431 - // Setup streaming headers
7432 - $this->setup_streaming_headers();
7433 -
7434 - $ch = curl_init();
7435 - curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/responses');
7436 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7437 - curl_setopt($ch, CURLOPT_POST, true);
7438 - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request_body));
7439 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7440 - 'Content-Type: application/json',
7441 - 'Authorization: Bearer ' . $api_key
7442 - ));
7443 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7444 - curl_setopt($ch, CURLOPT_TIMEOUT, 120);
7445 -
7446 - $full_response = '';
7447 - $stream_started = false;
7448 - $buffer = '';
7449 - $citations = [];
7450 -
7451 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$citations, $testing_data) {
7452 - // Send testing data as first event if available
7453 - if (!$stream_started && $testing_data !== null) {
7454 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7455 - flush();
7456 - $stream_started = true;
7457 - }
7458 -
7459 - $buffer .= $data;
7460 - $lines = explode("\n", $buffer);
7461 - $buffer = array_pop($lines);
7462 -
7463 - foreach ($lines as $line) {
7464 - if (trim($line) === '') continue;
7465 - if (strpos($line, 'data: ') !== 0) continue;
7466 -
7467 - $json_str = substr($line, 6);
7468 -
7469 - if (trim($json_str) === '[DONE]') {
7470 - // Append citations if we have any
7471 - if (!empty($citations)) {
7472 - $citation_text = "\n\n**Sources:**\n";
7473 - $seen_urls = [];
7474 - foreach ($citations as $citation) {
7475 - if (!in_array($citation['url'], $seen_urls)) {
7476 - $seen_urls[] = $citation['url'];
7477 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7478 - $citation_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7479 - }
7480 - }
7481 - echo "data: " . json_encode(['content' => $citation_text]) . "\n\n";
7482 - $full_response .= $citation_text;
7483 - flush();
7484 - }
7485 - echo "data: [DONE]\n\n";
7486 - flush();
7487 - continue;
7488 - }
7489 -
7490 - $json = json_decode(trim($json_str), true);
7491 - if (!$json) continue;
7492 -
7493 - // Handle Responses API streaming events
7494 - // The format is different from Chat Completions
7495 - if (isset($json['type'])) {
7496 - switch ($json['type']) {
7497 - case 'response.output_text.delta':
7498 - // Text content delta
7499 - if (isset($json['delta'])) {
7500 - $content = $json['delta'];
7501 - $full_response .= $content;
7502 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
7503 - flush();
7504 - }
7505 - break;
7506 -
7507 - case 'response.output_item.done':
7508 - // Check for citations in completed items
7509 - if (isset($json['item']['content'])) {
7510 - foreach ($json['item']['content'] as $content_item) {
7511 - if (isset($content_item['annotations'])) {
7512 - foreach ($content_item['annotations'] as $annotation) {
7513 - if ($annotation['type'] === 'url_citation') {
7514 - $citations[] = [
7515 - 'url' => $annotation['url'],
7516 - 'title' => $annotation['title'] ?? ''
7517 - ];
7518 - }
7519 - }
7520 - }
7521 - }
7522 - }
7523 - break;
7524 - }
7525 - }
7526 - }
7527 -
7528 - return strlen($data);
7529 - });
7530 -
7531 - $response = curl_exec($ch);
7532 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7533 -
7534 - if (curl_errno($ch) || $http_code !== 200) {
7535 - $curl_error = curl_error($ch);
7536 - curl_close($ch);
7537 -
7538 - //error_log("MXCHAT WEB SEARCH STREAM ERROR: HTTP $http_code, cURL error: $curl_error");
7539 -
7540 - // Fallback to non-streaming
7541 - $fallback_response = $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7542 -
7543 - if (is_array($fallback_response) && isset($fallback_response['error'])) {
7544 - echo "data: " . json_encode([
7545 - 'error' => true,
7546 - 'error_message' => $fallback_response['error'],
7547 - 'error_code' => $fallback_response['error_code'] ?? 'web_search_error'
7548 - ]) . "\n\n";
7549 - echo "data: [DONE]\n\n";
7550 - flush();
7551 - return true;
7552 - }
7553 -
7554 - $response_data = [
7555 - 'text' => $fallback_response,
7556 - 'html' => '',
7557 - 'session_id' => $session_id
7558 - ];
7559 - if ($testing_data !== null) {
7560 - $response_data['testing_data'] = $testing_data;
7561 - }
7562 - header('Content-Type: application/json');
7563 - echo json_encode($response_data);
7564 - return true;
7565 - }
7566 -
7567 - curl_close($ch);
7568 -
7569 - // Save the complete response with RAG context so the "sources" link
7570 - // appears in transcripts — mirrors the pattern used by Claude/OpenAI streaming.
7571 - if (!empty($full_response) && !empty($session_id)) {
7572 - $rag_context_for_storage = null;
7573 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7574 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7575 -
7576 - if ($has_rag_data || $has_action_data) {
7577 - $rag_context_for_storage = [];
7578 -
7579 - if ($has_rag_data) {
7580 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7581 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7582 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7583 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7584 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7585 - }
7586 -
7587 - if ($has_action_data) {
7588 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7589 - }
7590 - }
7591 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
7592 - }
7593 -
7594 - return true;
7595 -}
7596 -
7597 4711 private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7598 4712 try {
7599 4713 // Get bot ID from session or request
7600 4714 $bot_id = $this->get_current_bot_id($session_id);
@@ -7599,9 +4713,9 @@
7599 4713 // Get bot ID from session or request
7600 4714 $bot_id = $this->get_current_bot_id($session_id);
7601 4715
7602 4716 // Get system prompt instructions using centralized function
7603 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
4717 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
7604 4718 // Ensure conversation_history is an array
7605 4719 if (!is_array($conversation_history)) {
7606 4720 $conversation_history = array();
7607 4721 }
@@ -7653,13 +4767,8 @@
7653 4767 array_slice($conversation_history, 0, -1), // Remove the added content
7654 4768 $relevant_content
7655 4769 );
7656 4770
7657 - // Save bot response to transcript
7658 - if (!empty($regular_response) && !empty($session_id)) {
7659 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7660 - }
7661 -
7662 4771 // Return as JSON with testing data
7663 4772 $response_data = [
7664 4773 'text' => $regular_response,
7665 4774 'html' => '',
@@ -7678,11 +4787,8 @@
7678 4787 echo json_encode($response_data);
7679 4788 return true; // Indicate we handled the response
7680 4789 }
7681 4790
7682 - // Setup streaming headers now that we know we're actually streaming
7683 - $this->setup_streaming_headers();
7684 -
7685 4791 // Use cURL for streaming support
7686 4792 $ch = curl_init();
7687 4793 curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
7688 4794 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -7787,35 +4893,20 @@
7787 4893 $claude_api_key,
7788 4894 array_slice($conversation_history, 0, -1), // Remove the added content
7789 4895 $relevant_content
7790 4896 );
7791 -
7792 - // FIXED: Check if regular response returned an error
7793 - if (is_array($regular_response) && isset($regular_response['error'])) {
7794 - // Send error in SSE format since we're in streaming mode
7795 - echo "data: " . json_encode([
7796 - 'error' => true,
7797 - 'error_message' => $regular_response['error'],
7798 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7799 - 'text' => $regular_response['error'],
7800 - 'message' => $regular_response['error']
7801 - ]) . "\n\n";
7802 - echo "data: [DONE]\n\n";
7803 - flush();
7804 - return true;
7805 - }
7806 -
4897 +
7807 4898 $response_data = [
7808 4899 'text' => $regular_response,
7809 4900 'html' => '',
7810 4901 'session_id' => $session_id
7811 4902 ];
7812 -
4903 +
7813 4904 if ($testing_data !== null) {
7814 4905 $response_data['testing_data'] = $testing_data;
7815 4906 //error_log("MxChat Testing: Added testing data to Claude error fallback");
7816 4907 }
7817 -
4908 +
7818 4909 header('Content-Type: application/json');
7819 4910 echo json_encode($response_data);
7820 4911 return true;
7821 4912 }
@@ -7821,29 +4912,9 @@
7821 4912 }
7822 4913
7823 4914 // Save the complete response to maintain chat persistence
7824 4915 if (!empty($full_response) && !empty($session_id)) {
7825 - // Prepare RAG context for streaming response
7826 - $rag_context_for_storage = null;
7827 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7828 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7829 -
7830 - if ($has_rag_data || $has_action_data) {
7831 - $rag_context_for_storage = [];
7832 -
7833 - if ($has_rag_data) {
7834 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7835 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7836 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7837 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7838 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7839 - }
7840 -
7841 - if ($has_action_data) {
7842 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7843 - }
7844 - }
7845 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
4916 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
7846 4917 }
7847 4918
7848 4919 return true; // Indicate streaming completed successfully
7849 4920
@@ -7848,9 +4919,9 @@
7848 4919 return true; // Indicate streaming completed successfully
7849 4920
7850 4921 } catch (Exception $e) {
7851 4922 //error_log("MxChat Claude streaming exception: " . $e->getMessage());
7852 -
4923 +
7853 4924 // Fallback to regular response on exception
7854 4925 $regular_response = $this->mxchat_generate_response_claude(
7855 4926 $selected_model,
7856 4927 $claude_api_key,
@@ -7856,35 +4927,20 @@
7856 4927 $claude_api_key,
7857 4928 $conversation_history,
7858 4929 $relevant_content
7859 4930 );
7860 -
7861 - // FIXED: Check if regular response returned an error
7862 - if (is_array($regular_response) && isset($regular_response['error'])) {
7863 - // Send error in SSE format since we're in streaming mode
7864 - echo "data: " . json_encode([
7865 - 'error' => true,
7866 - 'error_message' => $regular_response['error'],
7867 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7868 - 'text' => $regular_response['error'],
7869 - 'message' => $regular_response['error']
7870 - ]) . "\n\n";
7871 - echo "data: [DONE]\n\n";
7872 - flush();
7873 - return true;
7874 - }
7875 -
4931 +
7876 4932 $response_data = [
7877 4933 'text' => $regular_response,
7878 4934 'html' => '',
7879 4935 'session_id' => $session_id
7880 4936 ];
7881 -
4937 +
7882 4938 if ($testing_data !== null) {
7883 4939 $response_data['testing_data'] = $testing_data;
7884 4940 //error_log("MxChat Testing: Added testing data to Claude exception fallback");
7885 4941 }
7886 -
4942 +
7887 4943 header('Content-Type: application/json');
7888 4944 echo json_encode($response_data);
7889 4945 return true;
7890 4946 }
@@ -7894,9 +4950,9 @@
7894 4950 // Get bot ID from session or request
7895 4951 $bot_id = $this->get_current_bot_id($session_id);
7896 4952
7897 4953 // Get system prompt instructions using centralized function
7898 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
4954 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
7899 4955
7900 4956 // Ensure conversation_history is an array
7901 4957 if (!is_array($conversation_history)) {
7902 4958 $conversation_history = array();
@@ -7936,13 +4992,8 @@
7936 4992 $conversation_history,
7937 4993 $relevant_content
7938 4994 );
7939 4995
7940 - // Save bot response to transcript
7941 - if (!empty($regular_response) && !empty($session_id)) {
7942 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7943 - }
7944 -
7945 4996 $response_data = [
7946 4997 'text' => $regular_response,
7947 4998 'html' => '',
7948 4999 'session_id' => $session_id
@@ -7965,11 +5016,8 @@
7965 5016 'temperature' => 0.8,
7966 5017 'stream' => true
7967 5018 ]);
7968 5019
7969 - // Setup streaming headers now that we know we're actually streaming
7970 - $this->setup_streaming_headers();
7971 -
7972 5020 // Use cURL for streaming support
7973 5021 $ch = curl_init();
7974 5022 curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
7975 5023 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -8070,39 +5118,19 @@
8070 5118 return true;
8071 5119 }
8072 5120
8073 5121 curl_close($ch);
8074 -
5122 +
8075 5123 // Save the complete response to maintain chat persistence
8076 5124 if (!empty($full_response) && !empty($session_id)) {
8077 - // Prepare RAG context for streaming response
8078 - $rag_context_for_storage = null;
8079 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8080 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8081 -
8082 - if ($has_rag_data || $has_action_data) {
8083 - $rag_context_for_storage = [];
8084 -
8085 - if ($has_rag_data) {
8086 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8087 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8088 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8089 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8090 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8091 - }
8092 -
8093 - if ($has_action_data) {
8094 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8095 - }
8096 - }
8097 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
5125 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
8098 5126 }
8099 -
5127 +
8100 5128 return true; // Indicate streaming completed successfully
8101 -
5129 +
8102 5130 } catch (Exception $e) {
8103 5131 //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
8104 -
5132 +
8105 5133 // Fallback to regular response
8106 5134 $regular_response = $this->mxchat_generate_response_xai(
8107 5135 $selected_model,
8108 5136 $xai_api_key,
@@ -8131,9 +5159,9 @@
8131 5159 // Get bot ID from session or request
8132 5160 $bot_id = $this->get_current_bot_id($session_id);
8133 5161
8134 5162 // Get system prompt instructions using centralized function
8135 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5163 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8136 5164
8137 5165 // Ensure conversation_history is an array
8138 5166 if (!is_array($conversation_history)) {
8139 5167 $conversation_history = array();
@@ -8173,13 +5201,8 @@
8173 5201 $conversation_history,
8174 5202 $relevant_content
8175 5203 );
8176 5204
8177 - // Save bot response to transcript
8178 - if (!empty($regular_response) && !empty($session_id)) {
8179 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
8180 - }
8181 -
8182 5205 $response_data = [
8183 5206 'text' => $regular_response,
8184 5207 'html' => '',
8185 5208 'session_id' => $session_id
@@ -8202,11 +5225,8 @@
8202 5225 'temperature' => 0.8,
8203 5226 'stream' => true
8204 5227 ]);
8205 5228
8206 - // Setup streaming headers now that we know we're actually streaming
8207 - $this->setup_streaming_headers();
8208 -
8209 5229 // Use cURL for streaming support
8210 5230 $ch = curl_init();
8211 5231 curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
8212 5232 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -8321,39 +5341,19 @@
8321 5341 return true;
8322 5342 }
8323 5343
8324 5344 curl_close($ch);
8325 -
5345 +
8326 5346 // Save the complete response to maintain chat persistence
8327 5347 if (!empty($full_response) && !empty($session_id)) {
8328 - // Prepare RAG context for streaming response
8329 - $rag_context_for_storage = null;
8330 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8331 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8332 -
8333 - if ($has_rag_data || $has_action_data) {
8334 - $rag_context_for_storage = [];
8335 -
8336 - if ($has_rag_data) {
8337 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8338 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8339 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8340 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8341 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8342 - }
8343 -
8344 - if ($has_action_data) {
8345 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8346 - }
8347 - }
8348 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
5348 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
8349 5349 }
8350 -
5350 +
8351 5351 return true; // Indicate streaming completed successfully
8352 -
5352 +
8353 5353 } catch (Exception $e) {
8354 5354 //error_log("MxChat DeepSeek streaming exception: " . $e->getMessage());
8355 -
5355 +
8356 5356 // Fallback to regular response
8357 5357 $regular_response = $this->mxchat_generate_response_deepseek(
8358 5358 $selected_model,
8359 5359 $deepseek_api_key,
@@ -8387,112 +5387,8 @@
8387 5387 return true;
8388 5388 }
8389 5389 }
8390 5390
8391 -
8392 -private function mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content) {
8393 - try {
8394 - if (!is_array($conversation_history)) {
8395 - $conversation_history = array();
8396 - }
8397 -
8398 - $bot_id = $this->get_current_bot_id('');
8399 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8400 -
8401 - $formatted_conversation = array();
8402 -
8403 - $formatted_conversation[] = array(
8404 - 'role' => 'system',
8405 - 'content' => $system_prompt_instructions . " " . $relevant_content
8406 - );
8407 -
8408 - foreach ($conversation_history as $message) {
8409 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8410 - $role = $message['role'];
8411 -
8412 - if ($role === 'bot' || $role === 'agent') {
8413 - $role = 'assistant';
8414 - }
8415 - if (!in_array($role, ['system', 'assistant', 'user'])) {
8416 - $role = 'user';
8417 - }
8418 -
8419 - $formatted_conversation[] = array(
8420 - 'role' => $role,
8421 - 'content' => $message['content']
8422 - );
8423 - }
8424 - }
8425 -
8426 - $body = json_encode([
8427 - 'model' => $selected_model,
8428 - 'messages' => $formatted_conversation,
8429 - 'temperature' => 1,
8430 - ]);
8431 -
8432 - $args = [
8433 - 'body' => $body,
8434 - 'headers' => [
8435 - 'Content-Type' => 'application/json',
8436 - 'Authorization' => 'Bearer ' . $openrouter_api_key,
8437 - 'HTTP-Referer' => home_url(),
8438 - 'X-Title' => get_bloginfo('name'),
8439 - ],
8440 - 'timeout' => 60,
8441 - 'redirection' => 5,
8442 - 'blocking' => true,
8443 - 'httpversion' => '1.0',
8444 - 'sslverify' => true,
8445 - ];
8446 -
8447 - $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', $args);
8448 -
8449 - if (is_wp_error($response)) {
8450 - $error_message = $response->get_error_message();
8451 - return [
8452 - 'error' => esc_html__('Connection error when contacting OpenRouter: ', 'mxchat') . esc_html($error_message),
8453 - 'error_code' => 'openrouter_connection_error',
8454 - 'provider' => 'openrouter'
8455 - ];
8456 - }
8457 -
8458 - $status_code = wp_remote_retrieve_response_code($response);
8459 - if ($status_code !== 200) {
8460 - $response_body = wp_remote_retrieve_body($response);
8461 - $decoded_response = json_decode($response_body, true);
8462 -
8463 - $error_message = isset($decoded_response['error']['message'])
8464 - ? $decoded_response['error']['message']
8465 - : 'HTTP Error ' . $status_code;
8466 -
8467 - return [
8468 - 'error' => esc_html__('OpenRouter API error: ', 'mxchat') . esc_html($error_message),
8469 - 'error_code' => 'openrouter_api_error',
8470 - 'provider' => 'openrouter',
8471 - 'status_code' => $status_code
8472 - ];
8473 - }
8474 -
8475 - $response_body = wp_remote_retrieve_body($response);
8476 - $decoded_response = json_decode($response_body, true);
8477 -
8478 - if (isset($decoded_response['choices'][0]['message']['content'])) {
8479 - return trim($decoded_response['choices'][0]['message']['content']);
8480 - } else {
8481 - return [
8482 - 'error' => esc_html__('Unexpected response format from OpenRouter.', 'mxchat'),
8483 - 'error_code' => 'openrouter_response_format_error',
8484 - 'provider' => 'openrouter'
8485 - ];
8486 - }
8487 - } catch (Exception $e) {
8488 - return [
8489 - 'error' => esc_html__('System error when processing OpenRouter request: ', 'mxchat') . esc_html($e->getMessage()),
8490 - 'error_code' => 'openrouter_exception',
8491 - 'provider' => 'openrouter'
8492 - ];
8493 - }
8494 -}
8495 5391 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
8496 5392
8497 5393 // Get bot ID from session or request
8498 5394 $bot_id = $this->get_current_bot_id($session_id);
@@ -8497,9 +5393,9 @@
8497 5393 // Get bot ID from session or request
8498 5394 $bot_id = $this->get_current_bot_id($session_id);
8499 5395
8500 5396 // Get system prompt instructions using centralized function
8501 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5397 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8502 5398
8503 5399 // Clean and validate conversation history
8504 5400 foreach ($conversation_history as &$message) {
8505 5401 // Convert bot and agent roles to assistant
@@ -8603,12 +5499,12 @@
8603 5499 $conversation_history = array();
8604 5500 }
8605 5501
8606 5502 // Get bot ID from session or request
8607 - $bot_id = $this->get_current_bot_id('');
5503 + $bot_id = $this->get_current_bot_id($session_id);
8608 5504
8609 5505 // Get system prompt instructions using centralized function
8610 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5506 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8611 5507
8612 5508 // Create a new array for the formatted conversation
8613 5509 $formatted_conversation = array();
8614 5510
@@ -8637,42 +5533,15 @@
8637 5533 );
8638 5534 }
8639 5535 }
8640 5536
8641 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
8642 - $is_gpt5_model = (
8643 - strpos($selected_model, 'gpt-5') === 0 ||
8644 - $selected_model === 'gpt-5.2' ||
8645 - $selected_model === 'gpt-5.1-2025-11-13' ||
8646 - $selected_model === 'gpt-5' ||
8647 - $selected_model === 'gpt-5-mini' ||
8648 - $selected_model === 'gpt-5-nano'
8649 - );
8650 -
8651 - // Build request body with optimal settings for fast responses
8652 - $request_body = [
5537 + $body = json_encode([
8653 5538 'model' => $selected_model,
8654 5539 'messages' => $formatted_conversation,
8655 5540 'temperature' => 1,
8656 5541 'stream' => false
8657 - ];
5542 + ]);
8658 5543
8659 - // Add reasoning_effort only for GPT-5 models that support it
8660 - // These chat models don't support reasoning_effort parameter
8661 - $no_reasoning_models = array('gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
8662 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
8663 - // GPT-5.1 uses 'low' instead of 'minimal'
8664 - if ($selected_model === 'gpt-5.1-2025-11-13') {
8665 - $request_body['reasoning_effort'] = 'low';
8666 - } elseif ($selected_model === 'gpt-5.4') {
8667 - $request_body['reasoning_effort'] = 'none';
8668 - } else {
8669 - $request_body['reasoning_effort'] = 'minimal';
8670 - }
8671 - }
8672 -
8673 - $body = json_encode($request_body);
8674 -
8675 5544 $args = [
8676 5545 'body' => $body,
8677 5546 'headers' => [
8678 5547 'Content-Type' => 'application/json',
@@ -8688,8 +5557,9 @@
8688 5557 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
8689 5558
8690 5559 if (is_wp_error($response)) {
8691 5560 $error_message = $response->get_error_message();
5561 + //error_log('OpenAI API Error: ' . $error_message);
8692 5562 return [
8693 5563 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
8694 5564 'error_code' => 'openai_connection_error',
8695 5565 'provider' => 'openai'
@@ -8708,8 +5578,10 @@
8708 5578 $error_type = isset($decoded_response['error']['type'])
8709 5579 ? $decoded_response['error']['type']
8710 5580 : 'unknown';
8711 5581
5582 + //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
5583 +
8712 5584 // Handle specific error types
8713 5585 switch ($error_type) {
8714 5586 case 'invalid_request_error':
8715 5587 if (strpos($error_message, 'API key') !== false) {
@@ -8757,8 +5629,9 @@
8757 5629
8758 5630 if (isset($decoded_response['choices'][0]['message']['content'])) {
8759 5631 return trim($decoded_response['choices'][0]['message']['content']);
8760 5632 } else {
5633 + //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
8761 5634 return [
8762 5635 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
8763 5636 'error_code' => 'openai_response_format_error',
8764 5637 'provider' => 'openai'
@@ -8764,8 +5637,9 @@
8764 5637 'provider' => 'openai'
8765 5638 ];
8766 5639 }
8767 5640 } catch (Exception $e) {
5641 + //error_log('OpenAI Exception: ' . $e->getMessage());
8768 5642 return [
8769 5643 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
8770 5644 'error_code' => 'openai_exception',
8771 5645 'provider' => 'openai'
@@ -8771,9 +5645,8 @@
8771 5645 'provider' => 'openai'
8772 5646 ];
8773 5647 }
8774 5648 }
8775 -
8776 5649 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
8777 5650 try {
8778 5651 // Get bot ID from session or request
8779 5652 $bot_id = $this->get_current_bot_id($session_id);
@@ -8778,9 +5651,9 @@
8778 5651 // Get bot ID from session or request
8779 5652 $bot_id = $this->get_current_bot_id($session_id);
8780 5653
8781 5654 // Get system prompt instructions using centralized function
8782 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5655 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8783 5656
8784 5657 // Add system prompt to relevant content
8785 5658 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
8786 5659
@@ -8977,9 +5850,9 @@
8977 5850 // Get bot ID from session or request
8978 5851 $bot_id = $this->get_current_bot_id($session_id);
8979 5852
8980 5853 // Get system prompt instructions using centralized function
8981 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5854 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8982 5855
8983 5856 // Create a new array for the formatted conversation
8984 5857 $formatted_conversation = array();
8985 5858
@@ -9136,9 +6009,9 @@
9136 6009 // Get bot ID from session or request
9137 6010 $bot_id = $this->get_current_bot_id($session_id);
9138 6011
9139 6012 // Get system prompt instructions using centralized function
9140 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6013 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
9141 6014
9142 6015 // Add system prompt to relevant content
9143 6016 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
9144 6017
@@ -9234,11 +6107,9 @@
9234 6107 ]
9235 6108 ]);
9236 6109
9237 6110 // Prepare the API endpoint
9238 - // Use v1beta for preview models (Gemini 3, experimental), v1 for stable models
9239 - $api_version = (strpos($selected_model, 'preview') !== false || strpos($selected_model, 'exp') !== false) ? 'v1beta' : 'v1';
9240 - $api_endpoint = 'https://generativelanguage.googleapis.com/' . $api_version . '/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
6111 + $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
9241 6112
9242 6113 // Set up the API request
9243 6114 $args = [
9244 6115 'body' => $body,
@@ -9280,9 +6151,9 @@
9280 6151
9281 6152
9282 6153 public function test_streaming_request() {
9283 6154 $options = get_option('mxchat_options', []);
9284 - $model = $options['model'] ?? 'gpt-5.1-chat-latest';
6155 + $model = $options['model'] ?? 'gpt-4o';
9285 6156
9286 6157 // Detect provider from model prefix
9287 6158 $provider = strtolower(explode('-', $model)[0]);
9288 6159
@@ -9465,54 +6336,35 @@
9465 6336 }
9466 6337
9467 6338
9468 6339 public function mxchat_enqueue_scripts_styles() {
9469 - // Fetch options from the database first to check loading strategy
9470 - $this->options = get_option('mxchat_options');
9471 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
9472 -
9473 - // Always enqueue CSS immediately
6340 + // Define version numbers for the styles and scripts
6341 + $chat_style_version = '2.4.6';
6342 + $chat_script_version = '2.4.6';
6343 + // Enqueue the script
6344 + wp_enqueue_script(
6345 + 'mxchat-chat-js',
6346 + plugin_dir_url(__FILE__) . '../js/chat-script.js',
6347 + array('jquery'),
6348 + $chat_script_version,
6349 + true
6350 + );
6351 + // Enqueue the CSS
9474 6352 wp_enqueue_style(
9475 6353 'mxchat-chat-css',
9476 6354 plugin_dir_url(__FILE__) . '../css/chat-style.css',
9477 6355 array(),
9478 - MXCHAT_VERSION
6356 + $chat_style_version
9479 6357 );
9480 -
9481 - // Handle script loading based on strategy
9482 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
9483 - // Enqueue the script normally
9484 - wp_enqueue_script(
9485 - 'mxchat-chat-js',
9486 - plugin_dir_url(__FILE__) . '../js/chat-script.js',
9487 - array('jquery'),
9488 - MXCHAT_VERSION,
9489 - true
9490 - );
9491 -
9492 - // Add defer attribute if strategy is 'defer'
9493 - if ($loading_strategy === 'defer') {
9494 - wp_script_add_data('mxchat-chat-js', 'strategy', 'defer');
9495 - }
9496 - } else {
9497 - // For delay or interaction-based loading, we'll use a custom loader
9498 - // Don't enqueue the main script - we'll load it dynamically
9499 - add_action('wp_footer', array($this, 'mxchat_output_delayed_script_loader'), 99);
9500 - }
9501 -
6358 + // Fetch options from the database
6359 + $this->options = get_option('mxchat_options');
9502 6360 $prompts_options = get_option('mxchat_prompts_options', array());
9503 -
9504 - // Check if AI theme is active - if so, skip inline colors in JavaScript
9505 - $theme_options = get_option('mxchat_theme_options', array());
9506 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9507 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9508 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9509 -
6361 +
9510 6362 // Prepare settings for JavaScript
9511 6363 $style_settings = array(
9512 6364 'ajax_url' => admin_url('admin-ajax.php'),
9513 6365 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9514 - 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
6366 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
9515 6367 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
9516 6368 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
9517 6369 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
9518 6370 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
@@ -9539,140 +6391,15 @@
9539 6391 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9540 6392 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off', // FIXED
9541 6393 'initial_email_state' => null, // Also fixed this undefined variable
9542 6394 'skip_email_check' => true,
9543 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9544 - 'skip_inline_colors' => $skip_inline_colors,
9545 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array(),
9546 - 'print_button_enabled' => $this->options['print_button_enabled'] ?? 'on',
9547 - 'print_button_label' => esc_html__('Download Transcript', 'mxchat'),
9548 - 'print_header_title' => esc_html(get_bloginfo('name')) . ' — ' . esc_html__('Chat transcript', 'mxchat'),
6395 + 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
9549 6396 );
9550 -
9551 - // For normal/defer loading, use wp_localize_script
9552 - // For delayed loading, we store settings in a transient to be output inline
9553 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
9554 - wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
9555 - } else {
9556 - // Store settings for the delayed loader to use
9557 - set_transient('mxchat_delayed_settings_' . get_current_user_id(), $style_settings, 60);
9558 - }
6397 + // Pass the settings to the script
6398 + wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
9559 6399 }
9560 6400
9561 -/**
9562 - * Output the delayed script loader for performance optimization
9563 - */
9564 -public function mxchat_output_delayed_script_loader() {
9565 - $this->options = get_option('mxchat_options');
9566 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
9567 - $script_url = plugin_dir_url(__FILE__) . '../js/chat-script.js?ver=' . MXCHAT_VERSION;
9568 6401
9569 - // Get the stored settings
9570 - $prompts_options = get_option('mxchat_prompts_options', array());
9571 - $theme_options = get_option('mxchat_theme_options', array());
9572 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9573 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9574 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9575 -
9576 - $style_settings = array(
9577 - 'ajax_url' => admin_url('admin-ajax.php'),
9578 - 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9579 - 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
9580 - 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
9581 - 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
9582 - 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
9583 - 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
9584 - 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
9585 - 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
9586 - 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
9587 - 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
9588 - 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
9589 - 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
9590 - 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
9591 - 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
9592 - 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
9593 - 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
9594 - 'icon_color' => $this->options['icon_color'] ?? '#fff',
9595 - 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
9596 - 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
9597 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
9598 - 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
9599 - 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
9600 - 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
9601 - 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
9602 - 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
9603 - 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
9604 - 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9605 - 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off',
9606 - 'initial_email_state' => null,
9607 - 'skip_email_check' => true,
9608 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9609 - 'skip_inline_colors' => $skip_inline_colors,
9610 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array(),
9611 - 'print_button_enabled' => $this->options['print_button_enabled'] ?? 'on',
9612 - 'print_button_label' => esc_html__('Download Transcript', 'mxchat'),
9613 - 'print_header_title' => esc_html(get_bloginfo('name')) . ' — ' . esc_html__('Chat transcript', 'mxchat'),
9614 - );
9615 -
9616 - // Determine delay time based on strategy
9617 - $delay_ms = 0;
9618 - switch ($loading_strategy) {
9619 - case 'delay_1s':
9620 - $delay_ms = 1000;
9621 - break;
9622 - case 'delay_3s':
9623 - $delay_ms = 3000;
9624 - break;
9625 - case 'delay_5s':
9626 - $delay_ms = 5000;
9627 - break;
9628 - }
9629 -
9630 - ?>
9631 - <script type="text/javascript">
9632 - (function() {
9633 - var mxchatLoaded = false;
9634 - var mxchatChat = <?php echo wp_json_encode($style_settings); ?>;
9635 - window.mxchatChat = mxchatChat;
9636 -
9637 - function loadMxChatScript() {
9638 - if (mxchatLoaded) return;
9639 - mxchatLoaded = true;
9640 -
9641 - function appendChatScript() {
9642 - var script = document.createElement('script');
9643 - script.src = <?php echo wp_json_encode($script_url); ?>;
9644 - script.type = 'text/javascript';
9645 - document.body.appendChild(script);
9646 - }
9647 -
9648 - if (typeof jQuery !== 'undefined') {
9649 - appendChatScript();
9650 - } else {
9651 - var jq = document.createElement('script');
9652 - jq.src = <?php echo wp_json_encode(includes_url('js/jquery/jquery.min.js')); ?>;
9653 - jq.onload = appendChatScript;
9654 - document.body.appendChild(jq);
9655 - }
9656 - }
9657 -
9658 - <?php if ($loading_strategy === 'on_interaction'): ?>
9659 - // Load on user interaction
9660 - var events = ['scroll', 'mousemove', 'touchstart', 'keydown', 'click'];
9661 - events.forEach(function(evt) {
9662 - window.addEventListener(evt, loadMxChatScript, {once: true, passive: true});
9663 - });
9664 - // Fallback: load after 8 seconds if no interaction
9665 - setTimeout(loadMxChatScript, 8000);
9666 - <?php else: ?>
9667 - // Load after specified delay
9668 - setTimeout(loadMxChatScript, <?php echo intval($delay_ms); ?>);
9669 - <?php endif; ?>
9670 - })();
9671 - </script>
9672 - <?php
9673 -}
9674 -
9675 6402 /**
9676 6403 * Setup the cron jobs for rate limits with guard against multiple calls
9677 6404 */
9678 6405 public function setup_rate_limit_cron_jobs() {
@@ -10207,11 +6934,8 @@
10207 6934
10208 6935 /**
10209 6936 * AJAX handler to get system information for testing panel
10210 6937 */
10211 -/**
10212 - * AJAX handler to get system information for testing panel
10213 - */
10214 6938 public function mxchat_get_system_info() {
10215 6939 // Verify nonce for security
10216 6940 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10217 6941 wp_send_json_error(['message' => 'Invalid nonce']);
@@ -10228,25 +6952,11 @@
10228 6952 $system_prompt = isset($this->options['system_prompt_instructions'])
10229 6953 ? $this->options['system_prompt_instructions']
10230 6954 : 'No system prompt configured';
10231 6955
10232 - // Get selected model
10233 - $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
6956 + // Get selected model - FIXED: Use $this->options instead of $current_options
6957 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
10234 6958
10235 - // Check if OpenRouter is being used
10236 - $is_openrouter = ($selected_model === 'openrouter');
10237 - $openrouter_model = '';
10238 -
10239 - if ($is_openrouter) {
10240 - // Get the actual OpenRouter model that's selected
10241 - $openrouter_model = isset($this->options['openrouter_selected_model'])
10242 - ? $this->options['openrouter_selected_model']
10243 - : 'No OpenRouter model selected';
10244 -
10245 - // Update selected_model display to show both
10246 - $selected_model = 'OpenRouter: ' . $openrouter_model;
10247 - }
10248 -
10249 6959 // Get API key status (just check if they exist, don't expose the keys)
10250 6960 $api_status = [];
10251 6961 $api_status['openai'] = !empty($this->options['api_key']);
10252 6962 $api_status['claude'] = !empty($this->options['claude_api_key']);
@@ -10252,15 +6962,12 @@
10252 6962 $api_status['claude'] = !empty($this->options['claude_api_key']);
10253 6963 $api_status['gemini'] = !empty($this->options['gemini_api_key']);
10254 6964 $api_status['xai'] = !empty($this->options['xai_api_key']);
10255 6965 $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
10256 - $api_status['openrouter'] = !empty($this->options['openrouter_api_key']);
10257 6966
10258 6967 wp_send_json_success([
10259 6968 'system_prompt' => $system_prompt,
10260 6969 'selected_model' => $selected_model,
10261 - 'is_openrouter' => $is_openrouter,
10262 - 'openrouter_model' => $openrouter_model,
10263 6970 'api_status' => $api_status
10264 6971 ]);
10265 6972 }
10266 6973
@@ -10299,42 +7006,24 @@
10299 7006 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10300 7007 wp_send_json_error(['message' => 'Invalid nonce']);
10301 7008 return;
10302 7009 }
10303 -
7010 +
10304 7011 // Only allow admin users
10305 7012 if (!current_user_can('administrator')) {
10306 7013 wp_send_json_error(['message' => 'Unauthorized']);
10307 7014 return;
10308 7015 }
10309 -
10310 - // Check OpenAI Vector Store first (takes priority)
10311 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
10312 - $use_vectorstore = (isset($vectorstore_options['mxchat_use_openai_vectorstore']) && $vectorstore_options['mxchat_use_openai_vectorstore'] === '1');
10313 -
10314 - if ($use_vectorstore) {
10315 - $vectorstore_ids = $vectorstore_options['mxchat_openai_vectorstore_ids'] ?? '';
10316 - $id_count = !empty($vectorstore_ids) ? count(array_filter(array_map('trim', explode(',', $vectorstore_ids)))) : 0;
10317 -
10318 - $kb_info = [
10319 - 'type' => 'OpenAI Vector Store',
10320 - 'status' => 'Active',
10321 - 'documents' => $id_count > 0 ? $id_count . ' vector store' . ($id_count > 1 ? 's' : '') . ' configured' : 'No vector stores configured'
10322 - ];
10323 -
10324 - wp_send_json_success($kb_info);
10325 - return;
10326 - }
10327 -
7016 +
10328 7017 // Check Pinecone vs WordPress
10329 7018 $addon_options = get_option('mxchat_pinecone_addon_options', array());
10330 7019 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
10331 -
7020 +
10332 7021 $kb_info = [
10333 7022 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
10334 7023 'status' => 'Active'
10335 7024 ];
10336 -
7025 +
10337 7026 // Get document count
10338 7027 if ($use_pinecone) {
10339 7028 $kb_info['documents'] = 'Connected to Pinecone';
10340 7029 $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
@@ -10344,9 +7033,9 @@
10344 7033 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
10345 7034 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
10346 7035 $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
10347 7036 }
10348 -
7037 +
10349 7038 wp_send_json_success($kb_info);
10350 7039 }
10351 7040
10352 7041 /**
@@ -10428,13 +7117,9 @@
10428 7117 // Clear any other session-specific transients
10429 7118 delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
10430 7119 delete_transient("mxchat_include_pdf_in_context_{$session_id}");
10431 7120 delete_transient("mxchat_include_word_in_context_{$session_id}");
10432 -
10433 - // Clear form addon state (pending forms and submitted forms)
10434 - delete_option("mxchat_pending_form_{$session_id}");
10435 - delete_option("mxchat_submitted_forms_{$session_id}");
10436 -
7121 +
10437 7122 //error_log("MxChat: Cleared all data for session: {$session_id}");
10438 7123 }
10439 7124
10440 7125 /**
@@ -10609,169 +7294,8 @@
10609 7294 wp_send_json_success(['message' => 'Originating page tracked']);
10610 7295 wp_die();
10611 7296 }
10612 7297
10613 -/**
10614 - * Validate and clean URLs from AI response
10615 - * Removes any URLs that aren't in the knowledge base
10616 - *
10617 - * @param string $response_text The AI-generated response
10618 - * @param array $valid_urls Array of URLs from the knowledge base
10619 - * @return string Cleaned response with invalid URLs removed/flagged
10620 - */
10621 -private function validate_and_clean_urls($response_text, $valid_urls) {
10622 - // DEBUG: Log what we're working with
10623 - //error_log("=== MxChat URL Validation Debug ===");
10624 - //error_log("Valid URLs count: " . count($valid_urls));
10625 - //error_log("Valid URLs: " . print_r($valid_urls, true));
10626 - //error_log("Response text length: " . strlen($response_text));
10627 - //error_log("Response text preview: " . substr($response_text, 0, 500));
10628 -
10629 - // If no valid URLs provided or empty response, return as-is
10630 - if (empty($valid_urls) || empty($response_text)) {
10631 - //error_log("Validation skipped - empty valid_urls or response");
10632 - return $response_text;
10633 - }
10634 -
10635 - // Extract all URLs from the AI response
10636 - // This regex matches http:// and https:// URLs
10637 - preg_match_all(
10638 - '#\bhttps?://[^\s<>"\')\]]+#i',
10639 - $response_text,
10640 - $matches
10641 - );
10642 -
10643 - // If no URLs found in response, return as-is
10644 - if (empty($matches[0])) {
10645 - //error_log("No URLs found in response");
10646 - return $response_text;
10647 - }
10648 -
10649 - $found_urls = $matches[0];
10650 - $cleaned_response = $response_text;
10651 - $removed_count = 0;
10652 -
10653 - // Normalize valid URLs for comparison (remove trailing slashes, fragments, etc.)
10654 - $normalized_valid_urls = array_map(function($url) {
10655 - // Remove trailing slash
10656 - $url = rtrim($url, '/');
10657 - // Remove URL fragments (#section)
10658 - $url = preg_replace('/#.*$/', '', $url);
10659 - // Remove trailing punctuation that might have been captured
10660 - $url = rtrim($url, '.,;:!?');
10661 - return $url;
10662 - }, $valid_urls);
10663 -
10664 - //error_log("Normalized valid URLs: " . print_r($normalized_valid_urls, true));
10665 -
10666 - foreach ($found_urls as $found_url) {
10667 - // Clean up the found URL (remove trailing punctuation that might have been captured)
10668 - $clean_found_url = rtrim($found_url, '.,;:!?)');
10669 -
10670 - // DEBUG: Log each URL being checked
10671 - //error_log("Checking found URL: " . $found_url);
10672 -
10673 - // Normalize for comparison
10674 - $normalized_found = rtrim($clean_found_url, '/');
10675 - $normalized_found = preg_replace('/#.*$/', '', $normalized_found);
10676 -
10677 - //error_log("Normalized found URL: " . $normalized_found);
10678 -
10679 - // Check if this URL exists in our valid URLs list
10680 - $is_valid = false;
10681 -
10682 - //error_log("Starting validation checks for: " . $normalized_found);
10683 -
10684 - // First, try exact match
10685 - if (in_array($normalized_found, $normalized_valid_urls)) {
10686 - $is_valid = true;
10687 - //error_log("EXACT MATCH FOUND");
10688 - } else {
10689 - //error_log("No exact match, checking variations...");
10690 - // If no exact match, check if it's a variation (with query params, etc.)
10691 - foreach ($normalized_valid_urls as $valid_url) {
10692 - //error_log(" Comparing against valid URL: " . $valid_url);
10693 -
10694 - // Check if the found URL starts with a valid URL (handles query params)
10695 - if (strpos($normalized_found, $valid_url) === 0) {
10696 - // Check what comes after the valid URL
10697 - $remainder = substr($normalized_found, strlen($valid_url));
10698 -
10699 - // Only valid if:
10700 - // 1. Exact match (remainder is empty)
10701 - // 2. Query params (starts with ?)
10702 - // 3. Fragment (starts with #)
10703 - if (empty($remainder) || $remainder[0] === '?' || $remainder[0] === '#') {
10704 - $is_valid = true;
10705 - //error_log(" MATCH: Found URL is valid variation of base URL");
10706 - break;
10707 - } else {
10708 - //error_log(" NOT A MATCH: Found URL extends path beyond valid URL (remainder: " . $remainder . ")");
10709 - }
10710 - }
10711 - // Also check the reverse (in case valid URL has query params)
10712 - if (strpos($valid_url, $normalized_found) === 0) {
10713 - $is_valid = true;
10714 - //error_log(" MATCH: Valid URL starts with found URL");
10715 - break;
10716 - }
10717 - }
10718 -
10719 - if (!$is_valid) {
10720 - //error_log("NO MATCH FOUND - URL should be removed");
10721 - }
10722 - }
10723 -
10724 - // If URL is not valid, remove it from the response
10725 - if (!$is_valid) {
10726 - // Log the removal for debugging
10727 - //error_log("MxChat: Removed hallucinated URL: " . $found_url);
10728 - //error_log("MxChat: Valid URLs were: " . implode(', ', array_slice($normalized_valid_urls, 0, 5)));
10729 -
10730 - $removed_count++;
10731 -
10732 - // Check if URL is part of a markdown link: [text](url)
10733 - $markdown_pattern = '/\[([^\]]+)\]\(' . preg_quote($found_url, '/') . '\)/';
10734 - if (preg_match($markdown_pattern, $cleaned_response)) {
10735 - //error_log("Found markdown link, removing but keeping text");
10736 - // Remove the markdown link but keep the text
10737 - $cleaned_response = preg_replace($markdown_pattern, '$1', $cleaned_response);
10738 - }
10739 - // Check if URL is part of an HTML link: <a href="url">text</a>
10740 - else if (preg_match('/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>(.*?)<\/a>/i', $cleaned_response, $link_match)) {
10741 - //error_log("Found HTML link, removing but keeping text");
10742 - // Remove the HTML link but keep the text
10743 - $link_text = $link_match[1];
10744 - $cleaned_response = preg_replace(
10745 - '/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>.*?<\/a>/i',
10746 - $link_text,
10747 - $cleaned_response
10748 - );
10749 - }
10750 - // Otherwise just remove the bare URL
10751 - else {
10752 - //error_log("Removing bare URL");
10753 - $cleaned_response = str_replace($found_url, '', $cleaned_response);
10754 - }
10755 - }
10756 - }
10757 -
10758 - // Log summary if any URLs were removed
10759 - if ($removed_count > 0) {
10760 - //error_log("MxChat: URL Validation Summary - Removed {$removed_count} hallucinated URL(s)");
10761 - } else {
10762 - //error_log("MxChat: URL Validation Summary - No URLs removed, all were valid");
10763 - }
10764 -
10765 - // Clean up any double spaces or awkward punctuation left behind
10766 - // IMPORTANT: Only collapse horizontal whitespace (spaces/tabs), preserve newlines for markdown formatting
10767 - $cleaned_response = preg_replace('/[^\S\n]+/', ' ', $cleaned_response); // Collapse spaces/tabs but NOT newlines
10768 - $cleaned_response = preg_replace('/[^\S\n]+([.,;:!?])/', '$1', $cleaned_response); // Same for punctuation cleanup
10769 -
10770 - //error_log("Final cleaned response: " . $cleaned_response);
10771 -
10772 - return trim($cleaned_response);
10773 -}
10774 7298
10775 7299 /**
10776 7300 * AJAX handler to get current chat mode for a session
10777 7301 */