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 -4056 3.2.12.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 = '';
@@ -1149,17 +800,8 @@
1149 800 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
1150 801 wp_die();
1151 802 }
1152 803
1153 - // Update session owner if it changed (e.g. IP changed due to network switch)
1154 - // The session ID itself is the authentication — if the client has it, they own it
1155 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
1156 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
1157 -
1158 - if (!$session_owner || $session_owner !== $current_user_identifier) {
1159 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
1160 - }
1161 -
1162 804 // Validate and sanitize the incoming message
1163 805 if (empty($_POST['message'])) {
1164 806 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
1165 807 wp_die();
@@ -1535,8 +1177,38 @@
1535 1177 }
1536 1178 }
1537 1179 }
1538 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 + }
1539 1211
1540 1212 // Step 2: Detect intent and handle intent-based responses
1541 1213 $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1542 1214
@@ -1555,46 +1227,58 @@
1555 1227 'text' => $intent_result['text'] ?? '',
1556 1228 'html' => $intent_result['html'] ?? '',
1557 1229 'session_id' => $session_id
1558 1230 ];
1559 -
1560 - // IMPORTANT: Include chat_mode if present (for WhatsApp, Slack, etc.)
1561 - if (isset($intent_result['chat_mode'])) {
1562 - $response_data['chat_mode'] = $intent_result['chat_mode'];
1563 - }
1564 -
1231 +
1565 1232 if ($testing_data !== null) {
1566 1233 $response_data['testing_data'] = $testing_data;
1567 1234 }
1568 -
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 +
1569 1245 wp_send_json($response_data);
1570 1246 wp_die();
1571 1247 } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1572 1248 // Intent returned true and set fallbackResponse
1573 -
1574 - // SAVE TO TRANSCRIPT
1249 +
1250 + // SAVE TO TRANSCRIPT FIRST
1575 1251 if (!empty($this->fallbackResponse['text'])) {
1576 1252 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1577 1253 }
1578 - // Save action HTML (product cards, featured products, etc.) so it renders in transcripts
1579 1254 if (!empty($this->fallbackResponse['html'])) {
1580 1255 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1581 1256 }
1582 -
1257 +
1583 1258 $response_data = [
1584 1259 'text' => $this->fallbackResponse['text'] ?? '',
1585 1260 'html' => $this->fallbackResponse['html'] ?? '',
1586 1261 'session_id' => $session_id
1587 1262 ];
1588 -
1263 +
1589 1264 if (isset($this->fallbackResponse['chat_mode'])) {
1590 1265 $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
1591 1266 }
1592 -
1267 +
1593 1268 if ($testing_data !== null) {
1594 1269 $response_data['testing_data'] = $testing_data;
1595 1270 }
1596 -
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 +
1597 1281 wp_send_json($response_data);
1598 1282 wp_die();
1599 1283 }
1600 1284 }
@@ -1599,13 +1283,11 @@
1599 1283 }
1600 1284 }
1601 1285
1602 1286 // If we get here, no intent matched OR the intent didn't provide a usable response
1603 -
1287 +
1604 1288 // Step 4: Generate AI response
1605 - // Get session start timestamp - when persistence is OFF, only include messages from this page load
1606 - $session_start_timestamp = isset($_POST['session_start_timestamp']) ? intval($_POST['session_start_timestamp']) : 0;
1607 - $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);
1608 1290 $this->mxchat_increment_chat_count();
1609 1291
1610 1292 // Generate embedding for the user's query - USE BOT-SPECIFIC API KEY
1611 1293 $api_key = $current_options['api_key'] ?? $this->options['api_key'];
@@ -1614,50 +1296,22 @@
1614 1296 // Check if the embedding generation returned an error
1615 1297 if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1616 1298 $error_message = $user_message_embedding['error'];
1617 1299 $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1618 -
1619 - // FIXED: Send error in appropriate format based on streaming mode
1620 - if ($is_streaming) {
1621 - echo "data: " . json_encode([
1622 - 'error' => true,
1623 - 'error_message' => $error_message,
1624 - 'error_code' => $error_code,
1625 - 'text' => $error_message,
1626 - 'message' => $error_message
1627 - ]) . "\n\n";
1628 - echo "data: [DONE]\n\n";
1629 - flush();
1630 - } else {
1631 - wp_send_json_error([
1632 - 'error_message' => $error_message,
1633 - 'error_code' => $error_code
1634 - ]);
1635 - }
1300 +
1301 + wp_send_json_error([
1302 + 'error_message' => $error_message,
1303 + 'error_code' => $error_code
1304 + ]);
1636 1305 wp_die();
1637 1306 }
1638 -
1307 +
1639 1308 // Check if the embedding is valid
1640 1309 if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1641 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
1642 -
1643 - // FIXED: Send error in appropriate format based on streaming mode
1644 - if ($is_streaming) {
1645 - echo "data: " . json_encode([
1646 - 'error' => true,
1647 - 'error_message' => $error_message,
1648 - 'error_code' => 'invalid_embedding',
1649 - 'text' => $error_message,
1650 - 'message' => $error_message
1651 - ]) . "\n\n";
1652 - echo "data: [DONE]\n\n";
1653 - flush();
1654 - } else {
1655 - wp_send_json_error([
1656 - 'error_message' => $error_message,
1657 - 'error_code' => 'invalid_embedding'
1658 - ]);
1659 - }
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 + ]);
1660 1314 wp_die();
1661 1315 }
1662 1316
1663 1317 // Build context with both knowledge base and PDF content if available
@@ -1683,71 +1337,26 @@
1683 1337 $context_content .= "Page Content: " . $page_context['content'] . "\n";
1684 1338 $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1685 1339 }
1686 1340
1687 - // Get relevant content from knowledge base - PASS BOT_ID and MESSAGE for Vector Store
1688 - $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id, $message);
1689 -
1690 - // NEW: Also extract URLs from system instructions (only if citation links enabled)
1691 - // Use fresh options to ensure we get the latest setting value
1692 - $fresh_options = get_option('mxchat_options', []);
1693 - $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);
1694 1343
1695 - $system_instructions = $this->get_system_instructions($bot_id, $session_id);
1696 - if ($citation_links_enabled && !empty($system_instructions)) {
1697 - preg_match_all(
1698 - '#\bhttps?://[^\s<>"\']+#i',
1699 - $system_instructions,
1700 - $system_instruction_urls
1701 - );
1702 -
1703 - if (!empty($system_instruction_urls[0])) {
1704 - // Merge with existing valid URLs
1705 - $this->current_valid_urls = array_merge(
1706 - $this->current_valid_urls,
1707 - $system_instruction_urls[0]
1708 - );
1709 - // Remove duplicates
1710 - $this->current_valid_urls = array_unique($this->current_valid_urls);
1711 -
1712 - //error_log("Added " . count($system_instruction_urls[0]) . " URLs from system instructions");
1713 - }
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'];
1714 1350 }
1351 + // ===== END SIMILARITY DATA CAPTURE =====
1715 1352
1716 -// ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1717 -if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1718 - // Update testing data with the REAL similarity analysis
1719 - $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1720 - $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1721 - $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1722 - $testing_data['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
1723 - $testing_data['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
1724 -}
1725 -// ===== END SIMILARITY DATA CAPTURE =====
1726 -
1727 -// NEW: Add valid URLs to testing data for admin panel display (AFTER similarity data)
1728 -if ($testing_data !== null && !empty($this->current_valid_urls)) {
1729 - $testing_data['approved_urls'] = array_values($this->current_valid_urls);
1730 - //error_log("Added " . count($this->current_valid_urls) . " approved URLs to testing data");
1731 -}
1732 -
1733 1353 if (!empty($relevant_content)) {
1734 1354 $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1735 1355 } else {
1736 1356 $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1737 1357 }
1738 -
1739 - // NEW: Add approved URLs list to context for AI (only if citation links enabled)
1740 - if ($citation_links_enabled && !empty($this->current_valid_urls)) {
1741 - $context_content .= "===== APPROVED URLS FOR CITATIONS =====\n";
1742 - $context_content .= "You may ONLY use these exact URLs in your response:\n";
1743 - foreach ($this->current_valid_urls as $url) {
1744 - $context_content .= "- " . $url . "\n";
1745 - }
1746 - $context_content .= "\nCRITICAL: Do NOT create, modify, extend, or invent any other URLs. ";
1747 - $context_content .= "===== END APPROVED URLS =====\n\n";
1748 - }
1749 -
1358 +
1750 1359 // Check for and include PDF content
1751 1360 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1752 1361 $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1753 1362 $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
@@ -1779,9 +1388,9 @@
1779 1388
1780 1389 $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1781 1390
1782 1391 // Extract model from current options for bot-specific model support
1783 - $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';
1784 1393
1785 1394 $response = $this->mxchat_generate_response(
1786 1395 $context_content,
1787 1396 $current_options['api_key'] ?? $this->options['api_key'],
@@ -1788,14 +1397,13 @@
1788 1397 $current_options['xai_api_key'] ?? $this->options['xai_api_key'],
1789 1398 $current_options['claude_api_key'] ?? $this->options['claude_api_key'],
1790 1399 $current_options['deepseek_api_key'] ?? $this->options['deepseek_api_key'],
1791 1400 $current_options['gemini_api_key'] ?? $this->options['gemini_api_key'],
1792 - $current_options['openrouter_api_key'] ?? $this->options['openrouter_api_key'],
1793 1401 $conversation_history,
1794 1402 $is_streaming,
1795 1403 $session_id,
1796 1404 $testing_data,
1797 - $selected_model
1405 + $selected_model // ADD THIS LINE
1798 1406 );
1799 1407
1800 1408 // Handle streaming vs non-streaming responses
1801 1409 if ($is_streaming) {
@@ -1803,27 +1411,11 @@
1803 1411 if ($response === true) {
1804 1412 wp_die();
1805 1413 }
1806 1414 // If we get here, streaming fell back to regular response, continue
1807 - // But if there's an error, we need to send it as SSE format since headers are already set
1808 - if (is_array($response) && isset($response['error'])) {
1809 - $error_message = $response['error'];
1810 - $error_code = $response['error_code'] ?? 'api_error';
1811 - // Send error in SSE format that the client JS can handle
1812 - echo "data: " . json_encode([
1813 - 'error' => true,
1814 - 'error_message' => $error_message,
1815 - 'error_code' => $error_code,
1816 - 'text' => $error_message, // Also include as text for fallback handling
1817 - 'message' => $error_message
1818 - ]) . "\n\n";
1819 - echo "data: [DONE]\n\n";
1820 - flush();
1821 - wp_die();
1822 - }
1823 1415 }
1824 -
1825 - // Check if the response is an error array (non-streaming mode)
1416 +
1417 + // Check if the response is an error array
1826 1418 if (is_array($response) && isset($response['error'])) {
1827 1419 wp_send_json_error([
1828 1420 'error_message' => $response['error'],
1829 1421 'error_code' => $response['error_code'] ?? 'api_error'
@@ -1830,51 +1422,11 @@
1830 1422 ]);
1831 1423 wp_die();
1832 1424 }
1833 1425
1834 - // DEBUG: Check what we have
1835 - //error_log("=== BEFORE URL VALIDATION ===");
1836 - //error_log("current_valid_urls is empty? " . (empty($this->current_valid_urls) ? 'YES' : 'NO'));
1837 - //error_log("current_valid_urls count: " . count($this->current_valid_urls));
1838 - //error_log("current_valid_urls content: " . print_r($this->current_valid_urls, true));
1839 -
1840 - // If we get here, the response is valid text - now validate URLs
1841 - if (!empty($this->current_valid_urls)) {
1842 - //error_log("CALLING validate_and_clean_urls");
1843 - $response = $this->validate_and_clean_urls($response, $this->current_valid_urls);
1844 - } else {
1845 - //error_log("SKIPPING validation - current_valid_urls is empty");
1846 - }
1847 - // ===== END URL VALIDATION =====
1426 + // If we get here, the response is valid text
1427 + $this->mxchat_save_chat_message($session_id, 'bot', $response);
1848 1428
1849 - // Prepare RAG context data for storage (only include documents used for context)
1850 - $rag_context_for_storage = null;
1851 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
1852 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
1853 -
1854 - if ($has_rag_data || $has_action_data) {
1855 - $rag_context_for_storage = [];
1856 -
1857 - // Add RAG/source data if available
1858 - if ($has_rag_data) {
1859 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
1860 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
1861 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
1862 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
1863 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1864 - $rag_context_for_storage['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
1865 - $rag_context_for_storage['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
1866 - }
1867 -
1868 - // Add action analysis data if available
1869 - if ($has_action_data) {
1870 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
1871 - }
1872 - }
1873 -
1874 - // Save the cleaned response with RAG context
1875 - $this->mxchat_save_chat_message($session_id, 'bot', $response, null, $rag_context_for_storage);
1876 -
1877 1429 // Step 5: Save additional content if available
1878 1430 if (!empty($this->productCardHtml)) {
1879 1431 $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1880 1432 }
@@ -1883,13 +1435,8 @@
1883 1435 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1884 1436 }
1885 1437
1886 1438 // Step 6: Return the response
1887 - // DEBUG: Check if newlines exist in the response
1888 - //error_log("=== MXCHAT NON-STREAMING RESPONSE DEBUG ===");
1889 - //error_log("Response has newlines: " . (strpos($response, "\n") !== false ? 'YES' : 'NO'));
1890 - //error_log("Response first 500 chars: " . substr($response, 0, 500));
1891 -
1892 1439 $response_data = [
1893 1440 'text' => $response,
1894 1441 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1895 1442 'session_id' => $session_id
@@ -1894,18 +1441,8 @@
1894 1441 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1895 1442 'session_id' => $session_id
1896 1443 ];
1897 1444
1898 - // Include vectorstore error info for admin debugging (only visible to admins via testing_data)
1899 - if (!empty($this->last_vectorstore_error) && $testing_data !== null) {
1900 - $testing_data['vectorstore_error'] = $this->last_vectorstore_error;
1901 - }
1902 -
1903 - // Also pass it as a top-level field so JS can show a better error message to admins
1904 - if (!empty($this->last_vectorstore_error) && current_user_can('manage_options')) {
1905 - $response_data['vectorstore_error'] = $this->last_vectorstore_error;
1906 - }
1907 -
1908 1445 // Always add testing data for admins (no toggle needed)
1909 1446 if ($testing_data !== null) {
1910 1447 $response_data['testing_data'] = $testing_data;
1911 1448 }
@@ -1913,8 +1450,9 @@
1913 1450 wp_send_json($response_data);
1914 1451 wp_die();
1915 1452 }
1916 1453
1454 +
1917 1455 /**
1918 1456 * Get bot-specific options for multi-bot functionality
1919 1457 * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1920 1458 */
@@ -1919,12 +1457,12 @@
1919 1457 * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1920 1458 */
1921 1459 // Also debug the bot options retrieval
1922 1460 private function get_bot_options($bot_id = 'default') {
1923 - //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);
1924 1462
1925 1463 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1926 - //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')");
1927 1465 return array();
1928 1466 }
1929 1467
1930 1468 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
@@ -1929,11 +1467,11 @@
1929 1467
1930 1468 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
1931 1469
1932 1470 if (!empty($bot_options)) {
1933 - //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1471 + error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1934 1472 if (isset($bot_options['similarity_threshold'])) {
1935 - //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1473 + error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1936 1474 }
1937 1475 }
1938 1476
1939 1477 return is_array($bot_options) ? $bot_options : array();
@@ -1944,13 +1482,13 @@
1944 1482 * Used in the knowledge retrieval functions
1945 1483 */
1946 1484 // Also add debugging to your get_bot_pinecone_config function
1947 1485 private function get_bot_pinecone_config($bot_id = 'default') {
1948 - //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);
1949 1487
1950 1488 // If default bot or multi-bot add-on not active, use default Pinecone config
1951 1489 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1952 - //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')");
1953 1491 $addon_options = get_option('mxchat_pinecone_addon_options', array());
1954 1492 $config = array(
1955 1493 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
1956 1494 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
@@ -1956,24 +1494,24 @@
1956 1494 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
1957 1495 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
1958 1496 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
1959 1497 );
1960 - //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'));
1961 1499 return $config;
1962 1500 }
1963 1501
1964 - //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);
1965 1503
1966 1504 // Hook for multi-bot add-on to provide bot-specific Pinecone config
1967 1505 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1968 1506
1969 1507 if (!empty($bot_pinecone_config)) {
1970 - //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1971 - //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1972 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1973 - //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'));
1974 1512 } else {
1975 - //error_log("MXCHAT DEBUG: Filter returned empty config!");
1513 + error_log("MXCHAT DEBUG: Filter returned empty config!");
1976 1514 }
1977 1515
1978 1516 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1979 1517 }
@@ -1983,63 +1521,35 @@
1983 1521 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
1984 1522 global $wpdb;
1985 1523 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1986 1524
1987 - // Get the current bot_id
1525 + // NEW: Get the current bot_id
1988 1526 $current_bot_id = $this->get_current_bot_id($session_id);
1989 1527
1990 1528 // Generate the user embedding
1991 1529 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1992 -
1530 +
1993 1531 // Check if embedding generation returned an error
1994 1532 if (is_array($user_embedding) && isset($user_embedding['error'])) {
1995 1533 $error_message = $user_embedding['error'];
1996 1534 $error_code = $user_embedding['error_code'] ?? 'embedding_error';
1997 -
1998 - // FIXED: Send error in appropriate format based on streaming mode
1999 - if ($this->is_streaming) {
2000 - echo "data: " . json_encode([
2001 - 'error' => true,
2002 - 'error_message' => $error_message,
2003 - 'error_code' => $error_code,
2004 - 'text' => $error_message,
2005 - 'message' => $error_message
2006 - ]) . "\n\n";
2007 - echo "data: [DONE]\n\n";
2008 - flush();
2009 - } else {
2010 - wp_send_json_error([
2011 - 'error_message' => $error_message,
2012 - 'error_code' => $error_code
2013 - ]);
2014 - }
1535 +
1536 + wp_send_json_error([
1537 + 'error_message' => $error_message,
1538 + 'error_code' => $error_code
1539 + ]);
2015 1540 wp_die();
2016 1541 }
2017 -
1542 +
2018 1543 // Check if embedding is valid
2019 1544 if (!is_array($user_embedding) || empty($user_embedding)) {
2020 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
2021 -
2022 - // FIXED: Send error in appropriate format based on streaming mode
2023 - if ($this->is_streaming) {
2024 - echo "data: " . json_encode([
2025 - 'error' => true,
2026 - 'error_message' => $error_message,
2027 - 'error_code' => 'invalid_embedding',
2028 - 'text' => $error_message,
2029 - 'message' => $error_message
2030 - ]) . "\n\n";
2031 - echo "data: [DONE]\n\n";
2032 - flush();
2033 - } else {
2034 - wp_send_json_error([
2035 - 'error_message' => $error_message,
2036 - 'error_code' => 'invalid_embedding'
2037 - ]);
2038 - }
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 + ]);
2039 1549 wp_die();
2040 1550 }
2041 -
1551 +
2042 1552 // Fetch intents from the database
2043 1553 $table_name = $wpdb->prefix . 'mxchat_intents';
2044 1554 if ($chat_mode === 'agent') {
2045 1555 $query = $wpdb->prepare(
@@ -2049,29 +1559,19 @@
2049 1559 $intents = $wpdb->get_results($query);
2050 1560 } else {
2051 1561 $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
2052 1562 }
2053 -
1563 +
2054 1564 if (empty($intents)) {
2055 1565 return false;
2056 1566 }
2057 -
2058 - // Prefetch individual phrase vectors from wp_mxchat_intent_phrases (grouped by intent_id)
2059 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
2060 - $phrases_by_intent = [];
2061 - if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
2062 - $all_phrases = $wpdb->get_results("SELECT intent_id, phrase, embedding_vector FROM $phrases_table");
2063 - foreach ($all_phrases as $p) {
2064 - $phrases_by_intent[$p->intent_id][] = $p;
2065 - }
2066 - }
2067 -
1567 +
2068 1568 $highest_similarity = -INF;
2069 1569 $matched_intent = null;
2070 -
1570 +
2071 1571 // Array to store action analysis for testing panel
2072 1572 $action_analysis = [];
2073 -
1573 +
2074 1574 foreach ($intents as $intent) {
2075 1575 // Additional check for enabled state
2076 1576 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2077 1577 if (!$is_enabled) {
@@ -2076,56 +1576,26 @@
2076 1576 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2077 1577 if (!$is_enabled) {
2078 1578 continue;
2079 1579 }
2080 -
2081 - // Check if this action is enabled for the current bot
1580 +
1581 + // NEW: Check if this action is enabled for the current bot
2082 1582 if (!$this->is_action_enabled_for_bot($intent, $current_bot_id)) {
2083 1583 continue;
2084 1584 }
2085 -
2086 - $best_similarity = -INF;
2087 - $matched_phrase_text = '';
2088 -
2089 - // Check legacy embedding vector (existing behavior)
1585 +
2090 1586 $intent_embedding_serialized = $intent->embedding_vector;
2091 1587 $intent_embedding = $intent_embedding_serialized
2092 1588 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
2093 1589 : null;
2094 -
2095 - if (is_array($intent_embedding) && !empty($intent_embedding)) {
2096 - $legacy_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2097 - if ($legacy_similarity > $best_similarity) {
2098 - $best_similarity = $legacy_similarity;
2099 - $matched_phrase_text = 'legacy';
2100 - }
2101 - }
2102 -
2103 - // Check individual phrase vectors
2104 - if (isset($phrases_by_intent[$intent->id])) {
2105 - foreach ($phrases_by_intent[$intent->id] as $phrase_row) {
2106 - $phrase_embedding = $phrase_row->embedding_vector
2107 - ? unserialize($phrase_row->embedding_vector, ['allowed_classes' => false])
2108 - : null;
2109 - if (!is_array($phrase_embedding)) {
2110 - continue;
2111 - }
2112 - $phrase_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $phrase_embedding);
2113 - if ($phrase_similarity > $best_similarity) {
2114 - $best_similarity = $phrase_similarity;
2115 - $matched_phrase_text = $phrase_row->phrase;
2116 - }
2117 - }
2118 - }
2119 -
2120 - // Skip if no valid embedding was found at all
2121 - if ($best_similarity === -INF) {
1590 +
1591 + if (!is_array($intent_embedding)) {
2122 1592 continue;
2123 1593 }
2124 -
2125 - $similarity = $best_similarity;
1594 +
1595 + $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2126 1596 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
2127 -
1597 +
2128 1598 // Store action analysis data for testing panel
2129 1599 $action_analysis[] = [
2130 1600 'intent_label' => $intent->intent_label,
2131 1601 'callback_function' => $intent->callback_function,
@@ -2133,12 +1603,11 @@
2133 1603 'similarity_percentage' => round($similarity * 100, 2),
2134 1604 'threshold' => $intent_threshold,
2135 1605 'threshold_percentage' => round($intent_threshold * 100, 2),
2136 1606 'above_threshold' => $similarity >= $intent_threshold,
2137 - 'matched_phrase' => $matched_phrase_text,
2138 1607 'triggered' => false // Will be updated below if this intent is triggered
2139 1608 ];
2140 -
1609 +
2141 1610 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
2142 1611 $highest_similarity = $similarity;
2143 1612 $matched_intent = $intent;
2144 1613 }
@@ -2209,22 +1678,16 @@
2209 1678 // If enabled_bots column doesn't exist or is null, assume it's enabled for all bots (backward compatibility)
2210 1679 if (!isset($intent->enabled_bots) || empty($intent->enabled_bots)) {
2211 1680 return true;
2212 1681 }
2213 -
1682 +
2214 1683 $enabled_bots = json_decode($intent->enabled_bots, true);
2215 -
1684 +
2216 1685 // If JSON decode fails or returns empty array, assume enabled for all (backward compatibility)
2217 1686 if (!is_array($enabled_bots) || empty($enabled_bots)) {
2218 1687 return true;
2219 1688 }
2220 -
2221 - // Admin testing tab uses bot_id "testing" — treat it as "default" so all
2222 - // default-bot actions are testable from the admin panel
2223 - if ($bot_id === 'testing') {
2224 - $bot_id = 'default';
2225 - }
2226 -
1689 +
2227 1690 // Check if the current bot is in the enabled bots list
2228 1691 return in_array($bot_id, $enabled_bots);
2229 1692 }
2230 1693
@@ -2263,17 +1726,17 @@
2263 1726
2264 1727 public function mxchat_generate_image($message, $user_id, $session_id) {
2265 1728 //error_log("Starting image generation for message: " . $message);
2266 1729
2267 - // Prepare a prompt for OpenAI image generation
1730 + // Prepare a prompt for DALL-E
2268 1731 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2269 -
1732 +
2270 1733 // Use the existing OpenAI API key
2271 1734 $openai_api_key = sanitize_text_field($this->options['api_key']);
2272 -
2273 - // Call OpenAI GPT Image to generate an image
2274 - $image_response = $this->mxchat_generate_openai_image($prompt, $openai_api_key);
2275 1735
1736 + // Call DALL-E to generate an image
1737 + $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1738 +
2276 1739 // Check if the response contains an image URL
2277 1740 if (isset($image_response['imageUrl'])) {
2278 1741 $image_url = esc_url_raw($image_response['imageUrl']);
2279 1742
@@ -2316,103 +1779,24 @@
2316 1779 // Return the response directly instead of relying on the property
2317 1780 return $this->fallbackResponse;
2318 1781 }
2319 1782 }
2320 -
2321 -public function mxchat_generate_gemini_image($message, $user_id, $session_id) {
2322 - $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2323 -
2324 - $gemini_api_key = sanitize_text_field($this->options['gemini_api_key'] ?? '');
2325 - if (empty($gemini_api_key)) {
2326 - $response_text = esc_html__("Gemini API key is not configured.", 'mxchat');
2327 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2328 - return ['text' => $response_text, 'html' => '', 'images' => []];
2329 - }
2330 -
2331 - $image_response = $this->mxchat_generate_imagen_image($prompt, $gemini_api_key);
2332 -
2333 - if (isset($image_response['imageUrl'])) {
2334 - $image_url = esc_url_raw($image_response['imageUrl']);
2335 -
2336 - $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
2337 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
2338 -
2339 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2340 - $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
2341 -
2342 - $this->fallbackResponse = [
2343 - 'text' => $response_text,
2344 - 'html' => $response_html,
2345 - 'images' => [$image_url]
2346 - ];
2347 -
2348 - return $this->fallbackResponse;
2349 - } else {
2350 - $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
2351 -
2352 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2353 -
2354 - $this->fallbackResponse = [
2355 - 'text' => $response_text,
2356 - 'html' => '',
2357 - 'images' => []
2358 - ];
2359 -
2360 - return $this->fallbackResponse;
2361 - }
2362 -}
2363 -
2364 -private function mxchat_save_generated_image($base64_data, $mime_type = 'image/png', $prefix = 'mxchat-generated') {
2365 - $extension = ($mime_type === 'image/jpeg') ? 'jpg' : 'png';
2366 - $filename = sanitize_file_name($prefix . '-' . wp_generate_uuid4() . '.' . $extension);
2367 - $decoded = base64_decode($base64_data);
2368 -
2369 - if ($decoded === false) {
2370 - return new \WP_Error('decode_failed', esc_html__('Failed to decode image data.', 'mxchat'));
2371 - }
2372 -
2373 - $upload = wp_upload_bits($filename, null, $decoded);
2374 -
2375 - if (!empty($upload['error'])) {
2376 - return new \WP_Error('upload_failed', $upload['error']);
2377 - }
2378 -
2379 - $attach_id = wp_insert_attachment([
2380 - 'post_mime_type' => $mime_type,
2381 - 'post_title' => $prefix,
2382 - 'post_content' => '',
2383 - 'post_status' => 'inherit',
2384 - ], $upload['file']);
2385 -
2386 - if (is_wp_error($attach_id)) {
2387 - return $attach_id;
2388 - }
2389 -
2390 - require_once ABSPATH . 'wp-admin/includes/image.php';
2391 - $metadata = wp_generate_attachment_metadata($attach_id, $upload['file']);
2392 - wp_update_attachment_metadata($attach_id, $metadata);
2393 -
2394 - return esc_url_raw(wp_get_attachment_url($attach_id));
2395 -}
2396 -
2397 -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) {
2398 1784 $api_url = 'https://api.openai.com/v1/images/generations';
2399 1785 $body = json_encode([
2400 - 'prompt' => sanitize_text_field($prompt),
2401 - 'n' => 1,
2402 - 'size' => '1024x1024',
2403 - 'quality' => 'medium',
2404 - 'output_format' => 'png',
2405 - 'model' => sanitize_text_field($model),
1786 + 'prompt' => sanitize_text_field($prompt),
1787 + 'n' => 1,
1788 + 'size' => '1024x1024',
1789 + 'model' => sanitize_text_field($model),
2406 1790 ]);
2407 1791
2408 1792 $args = [
2409 - 'body' => $body,
1793 + 'body' => $body,
2410 1794 'headers' => [
2411 - 'Content-Type' => 'application/json',
1795 + 'Content-Type' => 'application/json',
2412 1796 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
2413 1797 ],
2414 - 'method' => 'POST',
1798 + 'method' => 'POST',
2415 1799 'timeout' => absint($timeout),
2416 1800 ];
2417 1801
2418 1802 $response = wp_remote_post($api_url, $args);
@@ -2417,67 +1801,22 @@
2417 1801
2418 1802 $response = wp_remote_post($api_url, $args);
2419 1803
2420 1804 if (is_wp_error($response)) {
1805 + //error_log("DALL-E request failed: " . $response->get_error_message());
2421 1806 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2422 1807 }
2423 1808
2424 1809 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2425 1810
2426 - $b64 = $response_body['data'][0]['b64_json'] ?? $response_body['data'][0]['b64'] ?? null;
2427 - if ($b64) {
2428 - $saved_url = $this->mxchat_save_generated_image($b64, 'image/png', 'mxchat-openai');
2429 - if (is_wp_error($saved_url)) {
2430 - return ['error' => $saved_url->get_error_message()];
2431 - }
2432 - return ['imageUrl' => $saved_url];
1811 + if (isset($response_body['data'][0]['url'])) {
1812 + return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
2433 1813 } else {
1814 + //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
2434 1815 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2435 1816 }
2436 1817 }
2437 1818
2438 -private function mxchat_generate_imagen_image($prompt, $api_key, $timeout = 60) {
2439 - $api_url = 'https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict';
2440 -
2441 - $body = json_encode([
2442 - 'instances' => [['prompt' => sanitize_text_field($prompt)]],
2443 - 'parameters' => [
2444 - 'sampleCount' => 1,
2445 - 'aspectRatio' => '1:1',
2446 - ],
2447 - ]);
2448 -
2449 - $args = [
2450 - 'body' => $body,
2451 - 'headers' => [
2452 - 'Content-Type' => 'application/json',
2453 - 'x-goog-api-key' => sanitize_text_field($api_key),
2454 - ],
2455 - 'method' => 'POST',
2456 - 'timeout' => absint($timeout),
2457 - ];
2458 -
2459 - $response = wp_remote_post($api_url, $args);
2460 -
2461 - if (is_wp_error($response)) {
2462 - return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2463 - }
2464 -
2465 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
2466 -
2467 - $b64 = $response_body['predictions'][0]['bytesBase64Encoded'] ?? $response_body['predictions'][0]['imageBytes'] ?? null;
2468 - if ($b64) {
2469 - $mime = $response_body['predictions'][0]['mimeType'] ?? 'image/png';
2470 - $saved_url = $this->mxchat_save_generated_image($b64, $mime, 'mxchat-gemini');
2471 - if (is_wp_error($saved_url)) {
2472 - return ['error' => $saved_url->get_error_message()];
2473 - }
2474 - return ['imageUrl' => $saved_url];
2475 - } else {
2476 - return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2477 - }
2478 -}
2479 -
2480 1819 /**
2481 1820 * Handle web search requests.
2482 1821 *
2483 1822 * Sends the refined search query to the Brave Search API and uses the
@@ -2526,10 +1865,10 @@
2526 1865 $transient_key = 'mxchat_search_' . md5($refined_search_query);
2527 1866 $results = get_transient($transient_key);
2528 1867
2529 1868 if (false === $results) {
2530 - // SECURITY FIX: Changed to wp_safe_remote_get
2531 - $response = wp_safe_remote_get(
1869 + // Fetch new results from the Brave Search API
1870 + $response = wp_remote_get(
2532 1871 $api_url,
2533 1872 array(
2534 1873 'headers' => array(
2535 1874 'Accept' => 'application/json',
@@ -2668,10 +2007,9 @@
2668 2007 ],
2669 2008 'timeout' => 10,
2670 2009 ];
2671 2010
2672 - // SECURITY FIX: Changed to wp_safe_remote_get
2673 - $response = wp_safe_remote_get($api_url, $args);
2011 + $response = wp_remote_get($api_url, $args);
2674 2012
2675 2013 if (is_wp_error($response)) {
2676 2014 return array(
2677 2015 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
@@ -2744,9 +2082,9 @@
2744 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');
2745 2083
2746 2084 // Get options and determine the selected model
2747 2085 $options = $this->options ?? get_option('mxchat_options');
2748 - $selected_model = isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest';
2086 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
2749 2087
2750 2088 // Extract model prefix to determine the provider
2751 2089 $model_parts = explode('-', $selected_model);
2752 2090 $provider = strtolower($model_parts[0]);
@@ -2794,9 +2132,9 @@
2794 2132
2795 2133 /**
2796 2134 * Interpret query using OpenAI models
2797 2135 */
2798 -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') {
2799 2137 $url = 'https://api.openai.com/v1/chat/completions';
2800 2138 $args = [
2801 2139 'headers' => [
2802 2140 'Authorization' => 'Bearer ' . $api_key,
@@ -2867,13 +2205,13 @@
2867 2205 /**
2868 2206 * Interpret query using Gemini models
2869 2207 */
2870 2208 private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
2871 - // Use v1beta for preview models, v1 for stable models
2872 - $api_version = (strpos($model, 'preview') !== false || strpos($model, 'exp') !== false) ? 'v1beta' : 'v1';
2873 -
2874 - $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);
2875 2211
2212 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
2213 +
2876 2214 $args = [
2877 2215 'headers' => [
2878 2216 'Content-Type' => 'application/json',
2879 2217 ],
@@ -3069,9 +2407,9 @@
3069 2407 }
3070 2408
3071 2409
3072 2410 /**
3073 - * Enhanced fetch_and_split_pdf_pages with SSRF protection
2411 + * Enhanced fetch_and_split_pdf_pages with detailed debugging
3074 2412 */
3075 2413 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
3076 2414 // CLEAR DEBUG LOGGING
3077 2415 //error_log("=== MXCHAT PDF PROCESSING START ===");
@@ -3126,19 +2464,10 @@
3126 2464 // (I'll include the key parts with debug logging)
3127 2465
3128 2466 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
3129 2467 //error_log("Downloading PDF from URL...");
3130 -
3131 - // SECURITY FIX: Validate URL before processing
3132 - if (!$this->mxchat_is_safe_pdf_url($pdf_source)) {
3133 - //error_log("❌ SECURITY: Blocked unsafe PDF URL");
3134 - return false;
3135 - }
3136 -
3137 2468 $temp_file = wp_tempnam($pdf_source);
3138 -
3139 - // SECURITY FIX: Changed from wp_remote_get to wp_safe_remote_get
3140 - $response = wp_safe_remote_get($pdf_source, [
2469 + $response = wp_remote_get($pdf_source, [
3141 2470 'timeout' => 60,
3142 2471 'headers' => ['User-Agent' => 'MxChat PDF Processor']
3143 2472 ]);
3144 2473
@@ -3147,14 +2476,9 @@
3147 2476 //error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
3148 2477 return false;
3149 2478 }
3150 2479
3151 - global $wp_filesystem;
3152 - if (empty($wp_filesystem)) {
3153 - require_once ABSPATH . 'wp-admin/includes/file.php';
3154 - WP_Filesystem();
3155 - }
3156 - $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));
3157 2481 //error_log("✅ PDF downloaded successfully");
3158 2482 } else {
3159 2483 $temp_file = $pdf_source;
3160 2484 //error_log("Using local PDF file: " . $temp_file);
@@ -3161,9 +2485,8 @@
3161 2485 }
3162 2486
3163 2487 // Parse PDF
3164 2488 //error_log("Parsing PDF with basic parser...");
3165 - mxchat_load_pdf_parser();
3166 2489 $parser = new \Smalot\PdfParser\Parser();
3167 2490 $pdf = $parser->parseFile($temp_file);
3168 2491 $pages = $pdf->getPages();
3169 2492
@@ -3226,33 +2549,8 @@
3226 2549 return false;
3227 2550 }
3228 2551 }
3229 2552
3230 -
3231 -/**
3232 - * Validate PDF URL for security
3233 - * Prevents SSRF attacks by blocking dangerous URLs
3234 - */
3235 -
3236 -private function mxchat_is_safe_pdf_url($url) {
3237 - // Use WordPress core function for comprehensive validation
3238 - // This blocks localhost, private IPs, and reserved IP ranges
3239 - $validated_url = wp_http_validate_url($url);
3240 -
3241 - if ($validated_url === false) {
3242 - return false;
3243 - }
3244 -
3245 - // Additional check: only allow HTTP/HTTPS schemes
3246 - $parsed = parse_url($url);
3247 - if (!isset($parsed['scheme']) || !in_array($parsed['scheme'], ['http', 'https'], true)) {
3248 - return false;
3249 - }
3250 -
3251 - return true;
3252 -}
3253 -
3254 -
3255 2553 private function mxchat_clean_text($text) {
3256 2554 // Remove excessive whitespace
3257 2555 $text = preg_replace('/\s+/', ' ', $text);
3258 2556
@@ -3291,10 +2589,9 @@
3291 2589 }
3292 2590
3293 2591 return [];
3294 2592 }
3295 -
3296 -
2593 +// Add this to your class
3297 2594 public function handle_pdf_upload() {
3298 2595 check_ajax_referer('mxchat_chat_nonce', 'nonce');
3299 2596
3300 2597 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
@@ -3301,29 +2598,12 @@
3301 2598 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
3302 2599 return;
3303 2600 }
3304 2601
3305 - // SECURITY FIX: Check if PDF uploads are enabled in settings
3306 - $options = get_option('mxchat_options', array());
3307 - $show_pdf_button = isset($options['show_pdf_upload_button']) ? $options['show_pdf_upload_button'] : 'on';
3308 -
3309 - if ($show_pdf_button !== 'on') {
3310 - wp_send_json_error(esc_html__('PDF uploads are currently disabled.', 'mxchat'));
3311 - return;
3312 - }
3313 -
3314 2602 $file = $_FILES['pdf_file'];
3315 2603 $session_id = sanitize_text_field($_POST['session_id']);
3316 2604 $original_filename = sanitize_text_field($file['name']);
3317 2605
3318 - // Update session owner if it changed (e.g. IP changed due to network switch)
3319 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
3320 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
3321 -
3322 - if (!$session_owner || $session_owner !== $current_user_identifier) {
3323 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
3324 - }
3325 -
3326 2606 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
3327 2607 if ($file_type['type'] !== 'application/pdf') {
3328 2608 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
3329 2609 return;
@@ -3329,12 +2609,9 @@
3329 2609 return;
3330 2610 }
3331 2611
3332 2612 $upload_dir = wp_upload_dir();
3333 -
3334 - // SECURITY FIX: Generate random filename without exposing session_id
3335 - $random_string = wp_generate_password(20, false, false); // 20 char alphanumeric string
3336 - $pdf_filename = 'mxchat_' . $random_string . '_' . time() . '.pdf';
2613 + $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
3337 2614 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
3338 2615
3339 2616 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
3340 2617 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
@@ -3365,9 +2642,8 @@
3365 2642 return;
3366 2643 }
3367 2644
3368 2645 if (!empty($embeddings)) {
3369 - // Store the mapping between session and the random filename
3370 2646 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
3371 2647 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
3372 2648 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
3373 2649 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
@@ -3411,8 +2687,10 @@
3411 2687 wp_die();
3412 2688 }
3413 2689
3414 2690
2691 +
2692 +
3415 2693 function mxchat_fetch_new_messages() {
3416 2694 $session_id = sanitize_text_field($_POST['session_id']);
3417 2695 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
3418 2696 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
@@ -3425,31 +2703,14 @@
3425 2703 }
3426 2704
3427 2705 $history = get_option("mxchat_history_{$session_id}", []);
3428 2706
3429 - //error_log("MxChat WhatsApp DEBUG: Fetch new messages for session {$session_id}");
3430 - //error_log("MxChat WhatsApp DEBUG: last_seen_id = " . var_export($last_seen_id, true));
3431 - //error_log("MxChat WhatsApp DEBUG: History count = " . count($history));
3432 - //error_log("MxChat WhatsApp DEBUG: Full history = " . print_r($history, true));
3433 -
3434 2707 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
3435 - //error_log("MxChat WhatsApp DEBUG: Checking message - ID: " . ($message['id'] ?? 'NO_ID') . ", Role: " . ($message['role'] ?? 'NO_ROLE'));
3436 -
3437 2708 // If persistence is enabled, show all new messages
3438 2709 if ($persistence_enabled) {
3439 - $has_id = !empty($message['id']);
3440 - $is_agent = $message['role'] === 'agent';
3441 -
3442 - // If last_seen_id is empty, 'NaN', or invalid, show all agent messages
3443 - if (empty($last_seen_id) || $last_seen_id === 'NaN' || $last_seen_id === 'undefined') {
3444 - $is_newer = true;
3445 - } else {
3446 - $is_newer = strcmp($message['id'] ?? '', $last_seen_id) > 0;
3447 - }
3448 -
3449 - //error_log("MxChat WhatsApp DEBUG: has_id={$has_id}, is_newer={$is_newer}, is_agent={$is_agent}");
3450 -
3451 - return $has_id && $is_newer && $is_agent;
2710 + return !empty($message['id']) &&
2711 + strcmp($message['id'], $last_seen_id) > 0 &&
2712 + $message['role'] === 'agent';
3452 2713 }
3453 2714
3454 2715 // If persistence is disabled, only show messages after initial timestamp
3455 2716 return !empty($message['id']) &&
@@ -3456,16 +2717,12 @@
3456 2717 $message['role'] === 'agent' &&
3457 2718 $message['timestamp'] > $initial_timestamp;
3458 2719 });
3459 2720
3460 - //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'));
3461 2722
3462 - // Include current chat mode so frontend can detect agent→AI transitions
3463 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
3464 -
3465 2723 wp_send_json_success([
3466 - 'new_messages' => array_values($new_messages),
3467 - 'chat_mode' => $chat_mode
2724 + 'new_messages' => array_values($new_messages)
3468 2725 ]);
3469 2726 wp_die();
3470 2727 }
3471 2728 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
@@ -3750,393 +3007,9 @@
3750 3007
3751 3008 //error_log("[DEBUG] Generated channel name: {$channel_name}");
3752 3009 return $channel_name;
3753 3010 }
3754 -
3755 -/**
3756 - * Telegram Live Agent Handover
3757 - * Creates a forum topic in the Telegram group and notifies agents
3758 - */
3759 -public function mxchat_telegram_live_agent_handover($message, $user_id, $session_id) {
3760 - // Check if Telegram agents are available
3761 - $telegram_available = $this->options['telegram_status'] ?? 'off';
3762 - if ($telegram_available !== 'on') {
3763 - $away_message = $this->options['telegram_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
3764 - $this->fallbackResponse = [
3765 - 'text' => $away_message,
3766 - 'html' => '',
3767 - 'images' => [],
3768 - 'chat_mode' => 'ai'
3769 - ];
3770 - wp_send_json([
3771 - 'text' => $away_message,
3772 - 'html' => '',
3773 - 'chat_mode' => 'ai',
3774 - 'session_id' => $session_id
3775 - ]);
3776 - wp_die();
3777 - }
3778 -
3779 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
3780 - $telegram_group_id = $this->options['telegram_group_id'] ?? '';
3781 -
3782 - if (empty($telegram_bot_token) || empty($telegram_group_id)) {
3783 - return false;
3784 - }
3785 -
3786 - // Check if topic already exists for this session
3787 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
3788 -
3789 - if (empty($topic_id)) {
3790 - // Generate topic name
3791 - $topic_name = $this->generate_telegram_topic_name($session_id);
3792 -
3793 - // Random icon color (Telegram forum topic colors)
3794 - $icon_colors = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, 0xFB6F5F];
3795 - $icon_color = $icon_colors[array_rand($icon_colors)];
3796 -
3797 - // Create forum topic
3798 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/createForumTopic", [
3799 - 'headers' => ['Content-Type' => 'application/json'],
3800 - 'body' => json_encode([
3801 - 'chat_id' => $telegram_group_id,
3802 - 'name' => $topic_name,
3803 - 'icon_color' => $icon_color
3804 - ])
3805 - ]);
3806 -
3807 - if (!is_wp_error($response)) {
3808 - $response_body = wp_remote_retrieve_body($response);
3809 - $response_data = json_decode($response_body, true);
3810 -
3811 - if (isset($response_data['ok']) && $response_data['ok']) {
3812 - $topic_id = $response_data['result']['message_thread_id'];
3813 - update_option("mxchat_telegram_topic_{$session_id}", $topic_id);
3814 - update_option("mxchat_telegram_group_{$session_id}", $telegram_group_id);
3815 - }
3816 - }
3817 -
3818 - if (empty($topic_id)) {
3819 - return false; // Failed to create topic
3820 - }
3821 - }
3822 -
3823 - // Get recent chat history
3824 - $history = get_option("mxchat_history_{$session_id}", []);
3825 - $recent_history = array_slice($history, -5);
3826 -
3827 - // Format conversation context for Telegram (HTML format)
3828 - $conversation_context = "";
3829 - if (!empty($recent_history)) {
3830 - $conversation_context = "<b>Recent Conversation:</b>\n";
3831 - foreach ($recent_history as $hist_message) {
3832 - $role_display = $hist_message['role'] === 'user' ? '👤 User' : '🤖 AI';
3833 - $escaped_content = htmlspecialchars($hist_message['content'], ENT_QUOTES, 'UTF-8');
3834 - $conversation_context .= "{$role_display}: {$escaped_content}\n";
3835 - }
3836 - $conversation_context .= "\n";
3837 - }
3838 -
3839 - // Get user info
3840 - $user_email = get_option("mxchat_email_{$session_id}", 'Not provided');
3841 - $user_name = get_option("mxchat_name_{$session_id}", 'Anonymous');
3842 -
3843 - // Update session mode
3844 - update_option("mxchat_mode_{$session_id}", 'agent');
3845 -
3846 - // Send initial message to topic
3847 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3848 - $topic_message = "🔔 <b>New Live Agent Request</b>\n\n";
3849 - $topic_message .= "<b>Session ID:</b> <code>{$session_id}</code>\n";
3850 - $topic_message .= "<b>User:</b> {$user_name}\n";
3851 - $topic_message .= "<b>Email:</b> {$user_email}\n\n";
3852 -
3853 - if (!empty($conversation_context)) {
3854 - $topic_message .= $conversation_context;
3855 - }
3856 -
3857 - $topic_message .= "<b>Current Message:</b>\n{$escaped_message}\n\n";
3858 - $topic_message .= "<i>Reply in this topic - messages will be sent to the user</i>\n";
3859 - $topic_message .= "<i>Type #close, #end, #disconnect, or #done to end the session</i>";
3860 -
3861 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
3862 - 'headers' => ['Content-Type' => 'application/json'],
3863 - 'body' => json_encode([
3864 - 'chat_id' => $telegram_group_id,
3865 - 'message_thread_id' => $topic_id,
3866 - 'text' => $topic_message,
3867 - 'parse_mode' => 'HTML'
3868 - ])
3869 - ]);
3870 -
3871 - $success_message = $this->options['telegram_notification_message'] ?? "I've notified a support agent. Please allow a moment for them to respond.";
3872 - $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
3873 -
3874 - $this->fallbackResponse = [
3875 - 'text' => $success_message,
3876 - 'html' => '',
3877 - 'images' => [],
3878 - 'chat_mode' => 'agent'
3879 - ];
3880 -
3881 - wp_send_json([
3882 - 'success' => true,
3883 - 'text' => $success_message,
3884 - 'html' => '',
3885 - 'chat_mode' => 'agent',
3886 - 'session_id' => $session_id,
3887 - 'fallbackResponse' => $this->fallbackResponse
3888 - ]);
3889 - wp_die();
3890 -}
3891 -
3892 -/**
3893 - * Generate topic name for Telegram forum
3894 - */
3895 -private function generate_telegram_topic_name($session_id) {
3896 - $name = null;
3897 - $email = null;
3898 -
3899 - // Check logged in user
3900 - if (is_user_logged_in()) {
3901 - $current_user = wp_get_current_user();
3902 - if (!empty($current_user->display_name)) {
3903 - $name = $current_user->display_name;
3904 - }
3905 - if (!empty($current_user->user_email)) {
3906 - $email = $current_user->user_email;
3907 - }
3908 - }
3909 -
3910 - // Check session data
3911 - if (empty($name)) {
3912 - $name = get_option("mxchat_name_{$session_id}");
3913 - }
3914 - if (empty($email)) {
3915 - $email = get_option("mxchat_email_{$session_id}");
3916 - }
3917 -
3918 - // Generate topic name
3919 - $session_suffix = substr($session_id, -6);
3920 -
3921 - if (!empty($name)) {
3922 - // Clean name for topic (max 128 chars in Telegram)
3923 - $clean_name = preg_replace('/[^\p{L}\p{N}\s\-]/u', '', $name);
3924 - $clean_name = trim($clean_name);
3925 - if (strlen($clean_name) > 50) {
3926 - $clean_name = substr($clean_name, 0, 50);
3927 - }
3928 - return "Chat - {$clean_name} ({$session_suffix})";
3929 - } elseif (!empty($email)) {
3930 - // Use email prefix
3931 - $email_prefix = explode('@', $email)[0];
3932 - if (strlen($email_prefix) > 30) {
3933 - $email_prefix = substr($email_prefix, 0, 30);
3934 - }
3935 - return "Chat - {$email_prefix} ({$session_suffix})";
3936 - }
3937 -
3938 - return "Chat - {$session_suffix}";
3939 -}
3940 -
3941 -/**
3942 - * Send user message to Telegram agent
3943 - */
3944 -public function mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id) {
3945 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
3946 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
3947 - $group_id = get_option("mxchat_telegram_group_{$session_id}", '');
3948 -
3949 - if (empty($telegram_bot_token) || empty($topic_id) || empty($group_id)) {
3950 - return false;
3951 - }
3952 -
3953 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3954 - $user_message = "👤 <b>User:</b> {$escaped_message}";
3955 -
3956 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
3957 - 'headers' => ['Content-Type' => 'application/json'],
3958 - 'body' => json_encode([
3959 - 'chat_id' => $group_id,
3960 - 'message_thread_id' => $topic_id,
3961 - 'text' => $user_message,
3962 - 'parse_mode' => 'HTML'
3963 - ])
3964 - ]);
3965 -
3966 - return !is_wp_error($response);
3967 -}
3968 -
3969 -/**
3970 - * Handle incoming Telegram webhook
3971 - */
3972 -public function handle_telegram_webhook(WP_REST_Request $request) {
3973 - $body = $request->get_body();
3974 - $data = json_decode($body, true);
3975 -
3976 - //error_log('[MxChat Telegram DEBUG] Webhook received: ' . $body);
3977 -
3978 - // Handle message events from forum topics
3979 - if (isset($data['message'])) {
3980 - $message_data = $data['message'];
3981 -
3982 - // Skip if not from a forum topic
3983 - if (!isset($message_data['message_thread_id'])) {
3984 - //error_log('[MxChat Telegram DEBUG] Skipped: No message_thread_id (not a forum topic message)');
3985 - return new WP_REST_Response(['ok' => true]);
3986 - }
3987 -
3988 - // Skip bot messages
3989 - if (isset($message_data['from']['is_bot']) && $message_data['from']['is_bot']) {
3990 - //error_log('[MxChat Telegram DEBUG] Skipped: Message from bot');
3991 - return new WP_REST_Response(['ok' => true]);
3992 - }
3993 -
3994 - $chat_id = $message_data['chat']['id'] ?? '';
3995 - $topic_id = $message_data['message_thread_id'];
3996 - $message_text = $message_data['text'] ?? '';
3997 - $message_id = $message_data['message_id'] ?? '';
3998 - $from = $message_data['from'] ?? [];
3999 - $agent_name = trim(($from['first_name'] ?? '') . ' ' . ($from['last_name'] ?? ''));
4000 - if (empty($agent_name)) {
4001 - $agent_name = $from['username'] ?? 'Agent';
4002 - }
4003 -
4004 - //error_log("[MxChat Telegram DEBUG] Parsed: chat_id={$chat_id}, topic_id={$topic_id}, agent={$agent_name}, text={$message_text}");
4005 -
4006 - // Skip empty messages
4007 - if (empty($message_text)) {
4008 - //error_log('[MxChat Telegram DEBUG] Skipped: Empty message text');
4009 - return new WP_REST_Response(['ok' => true]);
4010 - }
4011 -
4012 - // Find session ID by topic ID - cast to string for comparison
4013 - global $wpdb;
4014 - $topic_id_str = strval($topic_id);
4015 - $session_option = $wpdb->get_var(
4016 - $wpdb->prepare(
4017 - "SELECT option_name FROM {$wpdb->options}
4018 - WHERE option_name LIKE %s
4019 - AND option_value = %s",
4020 - 'mxchat_telegram_topic_%',
4021 - $topic_id_str
4022 - )
4023 - );
4024 -
4025 - //error_log("[MxChat Telegram DEBUG] Looking for topic_id={$topic_id_str} in options, found: " . ($session_option ?: 'NULL'));
4026 -
4027 - if ($session_option) {
4028 - $session_id = str_replace('mxchat_telegram_topic_', '', $session_option);
4029 - //error_log("[MxChat Telegram DEBUG] Session ID: {$session_id}");
4030 -
4031 - // Verify the group ID matches
4032 - $stored_group_id = get_option("mxchat_telegram_group_{$session_id}", '');
4033 - //error_log("[MxChat Telegram DEBUG] Stored group_id={$stored_group_id}, received chat_id={$chat_id}");
4034 -
4035 - if (strval($stored_group_id) != strval($chat_id)) {
4036 - //error_log('[MxChat Telegram DEBUG] Skipped: Group ID mismatch');
4037 - return new WP_REST_Response(['ok' => true]);
4038 - }
4039 -
4040 - // Check for closure commands
4041 - $lower_text = strtolower(trim($message_text));
4042 - if (in_array($lower_text, ['#close', '#end', '#disconnect', '#done'])) {
4043 - //error_log("[MxChat Telegram DEBUG] Closure command received: {$lower_text}");
4044 - // End the live agent session
4045 - update_option("mxchat_mode_{$session_id}", 'ai');
4046 -
4047 - // Save disconnect message
4048 - $disconnect_message = "Live agent session ended. You're now chatting with the AI assistant.";
4049 - $this->mxchat_save_chat_message($session_id, 'bot', $disconnect_message);
4050 -
4051 - // Notify in Telegram
4052 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4053 - if (!empty($telegram_bot_token)) {
4054 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4055 - 'headers' => ['Content-Type' => 'application/json'],
4056 - 'body' => json_encode([
4057 - 'chat_id' => $chat_id,
4058 - 'message_thread_id' => $topic_id,
4059 - 'text' => "✅ Session closed. User returned to AI chatbot.",
4060 - 'parse_mode' => 'HTML'
4061 - ])
4062 - ]);
4063 -
4064 - // Optionally close the topic
4065 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/closeForumTopic", [
4066 - 'headers' => ['Content-Type' => 'application/json'],
4067 - 'body' => json_encode([
4068 - 'chat_id' => $chat_id,
4069 - 'message_thread_id' => $topic_id
4070 - ])
4071 - ]);
4072 - }
4073 -
4074 - return new WP_REST_Response(['ok' => true]);
4075 - }
4076 -
4077 - // Deduplicate messages
4078 - $message_key = md5($session_id . $message_id . $message_text);
4079 - $processed_messages = get_transient('mxchat_telegram_messages_' . $session_id) ?: [];
4080 -
4081 - if (in_array($message_key, $processed_messages)) {
4082 - //error_log('[MxChat Telegram DEBUG] Skipped: Duplicate message');
4083 - return new WP_REST_Response(['ok' => true]);
4084 - }
4085 -
4086 - $processed_messages[] = $message_key;
4087 - if (count($processed_messages) > 50) {
4088 - $processed_messages = array_slice($processed_messages, -50);
4089 - }
4090 - set_transient('mxchat_telegram_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4091 -
4092 - // Save the agent message - format with agent name prefix for proper parsing
4093 - $formatted_message = "Agent: {$agent_name} - {$message_text}";
4094 - //error_log("[MxChat Telegram DEBUG] Saving agent message: {$formatted_message}");
4095 -
4096 - $this->mxchat_save_chat_message($session_id, 'agent', $formatted_message);
4097 -
4098 - // Verify the message was saved to history
4099 - $history = get_option("mxchat_history_{$session_id}", []);
4100 - $last_message = end($history);
4101 - //error_log("[MxChat Telegram DEBUG] History after save - count: " . count($history) . ", last message role: " . ($last_message['role'] ?? 'none'));
4102 -
4103 - // Send confirmation back to Telegram
4104 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4105 - if (!empty($telegram_bot_token)) {
4106 - $confirm_key = 'mxchat_telegram_confirm_' . $message_key;
4107 - if (!get_transient($confirm_key)) {
4108 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4109 - 'headers' => ['Content-Type' => 'application/json'],
4110 - 'body' => json_encode([
4111 - 'chat_id' => $chat_id,
4112 - 'message_thread_id' => $topic_id,
4113 - 'text' => "✅ <i>Message sent to user</i>",
4114 - 'parse_mode' => 'HTML',
4115 - 'reply_to_message_id' => $message_id
4116 - ])
4117 - ]);
4118 - set_transient($confirm_key, true, 300);
4119 - }
4120 - }
4121 - } else {
4122 - //error_log("[MxChat Telegram DEBUG] No session found for topic_id={$topic_id}");
4123 - }
4124 - } else {
4125 - //error_log('[MxChat Telegram DEBUG] No message in webhook data');
4126 - }
4127 -
4128 - return new WP_REST_Response(['ok' => true]);
4129 -}
4130 -
4131 3011 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
4132 - // Check if this is a Telegram agent session
4133 - $telegram_topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4134 - if (!empty($telegram_topic_id)) {
4135 - return $this->mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id);
4136 - }
4137 -
4138 - // Otherwise, try Slack
4139 3012 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4140 3013 $channel_id = get_option("mxchat_channel_{$session_id}", '');
4141 3014
4142 3015 if (empty($slack_bot_token) || empty($channel_id)) {
@@ -4362,33 +3235,33 @@
4362 3235
4363 3236 $channel_id = $event['channel'];
4364 3237 $message_text = $event['text'] ?? '';
4365 3238 $message_ts = $event['ts'] ?? '';
4366 -
3239 +
4367 3240 // Find session ID by looking for matching channel
4368 3241 global $wpdb;
4369 3242 $session_option = $wpdb->get_var(
4370 3243 $wpdb->prepare(
4371 - "SELECT option_name FROM {$wpdb->options}
4372 - WHERE option_name LIKE 'mxchat_channel_%'
3244 + "SELECT option_name FROM {$wpdb->options}
3245 + WHERE option_name LIKE 'mxchat_channel_%'
4373 3246 AND option_value = %s",
4374 3247 $channel_id
4375 3248 )
4376 3249 );
4377 -
3250 +
4378 3251 if ($session_option) {
4379 3252 $session_id = str_replace('mxchat_channel_', '', $session_option);
4380 -
3253 +
4381 3254 // Create a unique key for this specific message
4382 3255 $message_key = md5($session_id . $message_ts . $message_text);
4383 3256 $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
4384 -
3257 +
4385 3258 // Check if we've already processed this exact message
4386 3259 if (in_array($message_key, $processed_messages)) {
4387 3260 //error_log("Duplicate message detected for session $session_id");
4388 3261 return new WP_REST_Response(['ok' => true]);
4389 3262 }
4390 -
3263 +
4391 3264 // Add to processed messages
4392 3265 $processed_messages[] = $message_key;
4393 3266 // Keep only last 50 messages per session
4394 3267 if (count($processed_messages) > 50) {
@@ -4394,46 +3267,14 @@
4394 3267 if (count($processed_messages) > 50) {
4395 3268 $processed_messages = array_slice($processed_messages, -50);
4396 3269 }
4397 3270 set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4398 -
4399 - $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4400 -
4401 - // Handle agent ending the chat — transfer back to AI
4402 - // Format: "!endchat" or "!endchat <custom message to user>"
4403 - if (preg_match('/^!endchat\b/i', trim($message_text))) {
4404 - update_option("mxchat_mode_{$session_id}", 'ai');
4405 -
4406 - // Extract custom message after !endchat, or use empty string
4407 - $custom_message = trim(preg_replace('/^!endchat\s*/i', '', trim($message_text)));
4408 -
4409 - // Send the agent's custom farewell message if provided
4410 - if (!empty($custom_message)) {
4411 - $this->mxchat_save_chat_message($session_id, 'agent', $custom_message);
4412 - }
4413 -
4414 - // Confirm in Slack channel
4415 - if (!empty($slack_bot_token)) {
4416 - wp_remote_post('https://slack.com/api/chat.postMessage', [
4417 - 'headers' => [
4418 - 'Content-Type' => 'application/json',
4419 - 'Authorization' => 'Bearer ' . $slack_bot_token
4420 - ],
4421 - 'body' => json_encode([
4422 - 'channel' => $channel_id,
4423 - 'text' => "✅ *Chat ended.* User has been transferred back to AI mode.",
4424 - 'mrkdwn' => true
4425 - ])
4426 - ]);
4427 - }
4428 -
4429 - return new WP_REST_Response(['ok' => true]);
4430 - }
4431 -
3271 +
4432 3272 // Save the agent message
4433 3273 $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
4434 -
3274 +
4435 3275 // Send confirmation back to Slack (only once)
3276 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4436 3277 if (!empty($slack_bot_token)) {
4437 3278 // Use a transient to prevent duplicate confirmations
4438 3279 $confirm_key = 'mxchat_confirm_' . $message_key;
4439 3280 if (!get_transient($confirm_key)) {
@@ -4443,9 +3284,9 @@
4443 3284 'Authorization' => 'Bearer ' . $slack_bot_token
4444 3285 ],
4445 3286 'body' => json_encode([
4446 3287 'channel' => $channel_id,
4447 - 'text' => "✅ _Message sent to user_",
3288 + 'text' => "✅ _Message sent to user_",
4448 3289 'thread_ts' => $event['ts'] // Reply in thread
4449 3290 ])
4450 3291 ]);
4451 3292 // Set transient to prevent duplicate confirmations
@@ -4684,44 +3525,26 @@
4684 3525 }
4685 3526 }
4686 3527
4687 3528
4688 -private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default', $user_query = '') {
4689 - //error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
4690 -
4691 - // Check for OpenAI Vector Store first (takes priority when enabled)
4692 - $bot_vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
4693 -
4694 - if ($bot_vectorstore_config['use_vectorstore']) {
4695 - // Get current model to verify it's an OpenAI model
4696 - $bot_options = $this->get_bot_options($bot_id);
4697 - $mxchat_options = get_option('mxchat_options', array());
4698 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
4699 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
4700 -
4701 - if ($this->is_openai_chat_model($selected_model)) {
4702 - //error_log("MXCHAT DEBUG: Using OpenAI Vector Store for knowledge retrieval");
4703 - return $this->find_relevant_content_openai_vectorstore($user_query, $bot_id, $bot_vectorstore_config);
4704 - } else {
4705 - //error_log("MXCHAT DEBUG: Vector Store enabled but model is not OpenAI (" . $selected_model . "), skipping Vector Store");
4706 - }
4707 - }
4708 -
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 +
4709 3532 // Get bot-specific Pinecone configuration
4710 3533 $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
4711 -
3534 +
4712 3535 // Debug: Log the Pinecone configuration
4713 - //error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
4714 - //error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
4715 - //error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
4716 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
4717 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
4718 -
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 +
4719 3542 // Determine whether to use Pinecone based on bot configuration
4720 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");
4721 3546
4722 - //error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
4723 -
4724 3547 if ($use_pinecone) {
4725 3548 return $this->find_relevant_content_pinecone($user_embedding, $bot_id, $bot_pinecone_config);
4726 3549 } else {
4727 3550 return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
@@ -4730,482 +3553,233 @@
4730 3553
4731 3554 private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
4732 3555 global $wpdb;
4733 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 +
4734 3560 // Initialize similarity analysis storage
4735 3561 $this->last_similarity_analysis = [
4736 3562 'knowledge_base_type' => 'WordPress Database',
4737 - 'bot_id' => $bot_id,
3563 + 'bot_id' => $bot_id, // Track which bot is being used
4738 3564 'top_matches' => [],
4739 3565 'threshold_used' => 0,
4740 3566 'total_checked' => 0
4741 3567 ];
4742 3568
4743 - // NEW: Initialize valid URLs array
4744 - $valid_urls = [];
4745 -
4746 - // Get bot-specific options for similarity threshold
3569 + // Get bot-specific options for similarity threshold
4747 3570 $bot_options = $this->get_bot_options($bot_id);
4748 3571 $current_options = !empty($bot_options) ? $bot_options : $this->options;
4749 3572
4750 - // Get knowledge manager instance for role checking
4751 - $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;
4752 3579
4753 - // Get base similarity threshold from bot options or default options
4754 - $similarity_threshold = isset($current_options['similarity_threshold'])
4755 - ? ((int) $current_options['similarity_threshold']) / 100
4756 - : 0.35;
4757 - $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 + }
4758 3590
4759 - // Precompute bot_filter once, outside the streaming loop
4760 - $bot_filter = '';
4761 - if ($bot_id !== 'default') {
4762 - $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
4763 - if ($column_exists) {
4764 - $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
4765 - }
4766 - }
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 + );
4767 3599
4768 - // ===== STREAMING TOP-K PASS =====
4769 - // Stream rows in small batches, compute cosine similarity per row, and keep only:
4770 - // - top 10 by raw similarity (for the testing/debug display panel)
4771 - // - candidates above threshold with access (capped) for context assembly
4772 - // This bounds peak memory regardless of knowledge base size and avoids loading
4773 - // article_content for every row. article_content is fetched in Phase 2 for winners only.
4774 - $batch_size = 250;
4775 - $max_candidates = 200; // safety cap, well above rag_sources_limit * max_chunks_per_source
4776 - $top_display = [];
4777 - $candidates = [];
4778 - $total_checked = 0;
4779 - $offset = 0;
3600 + $batch = $wpdb->get_results($query);
3601 + if (empty($batch)) {
3602 + break;
3603 + }
4780 3604
4781 - do {
4782 - $batch = $wpdb->get_results($wpdb->prepare(
4783 - "SELECT id, embedding_vector, source_url, role_restriction
4784 - FROM {$system_prompt_table}
4785 - WHERE 1=1 {$bot_filter}
4786 - LIMIT %d OFFSET %d",
4787 - $batch_size,
4788 - $offset
4789 - ));
3605 + $embeddings = array_merge($embeddings, $batch);
3606 + $offset += $batch_size;
3607 + unset($batch);
3608 + } while (true);
4790 3609
4791 - if (empty($batch)) {
4792 - break;
3610 + if (empty($embeddings)) {
3611 + return '';
4793 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 + }
4794 3617
4795 - foreach ($batch as $row) {
4796 - $database_embedding = $row->embedding_vector
4797 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
4798 - : null;
3618 + // Get knowledge manager instance for role checking
3619 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
4799 3620
4800 - if (!is_array($database_embedding) || !is_array($user_embedding)) {
4801 - unset($database_embedding);
4802 - continue;
4803 - }
4804 -
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)) {
4805 3638 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
4806 - unset($database_embedding);
4807 -
4808 - $role_restriction = $row->role_restriction ?? 'public';
3639 +
3640 + // Check role access
3641 + $role_restriction = $embedding->role_restriction ?? 'public';
4809 3642 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
4810 - $source_url = $row->source_url ?? '';
4811 -
4812 - // Maintain top 10 display buffer (insert-if-beats-worst)
4813 - if (count($top_display) < 10) {
4814 - $top_display[] = [
4815 - 'id' => $row->id,
4816 - 'similarity' => $similarity,
4817 - 'source_url' => $source_url,
4818 - 'role_restriction' => $role_restriction,
4819 - 'has_access' => $has_access,
4820 - ];
4821 - usort($top_display, function ($a, $b) {
4822 - return $b['similarity'] <=> $a['similarity'];
4823 - });
4824 - } elseif ($similarity > $top_display[9]['similarity']) {
4825 - $top_display[9] = [
4826 - 'id' => $row->id,
4827 - 'similarity' => $similarity,
4828 - 'source_url' => $source_url,
4829 - 'role_restriction' => $role_restriction,
4830 - 'has_access' => $has_access,
4831 - ];
4832 - usort($top_display, function ($a, $b) {
4833 - return $b['similarity'] <=> $a['similarity'];
4834 - });
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) . '...';
4835 3652 }
4836 -
4837 - // 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
4838 3668 if ($similarity >= $similarity_threshold && $has_access) {
4839 - $candidates[] = [
4840 - 'id' => $row->id,
4841 - 'similarity' => $similarity,
4842 - 'source_url' => $source_url,
3669 + $relevant_results[] = [
3670 + 'id' => $embedding->id,
3671 + 'similarity' => $similarity
4843 3672 ];
4844 3673 }
4845 -
4846 - $total_checked++;
4847 3674 }
4848 -
4849 - unset($batch);
4850 -
4851 - // Trim candidates periodically to cap memory during long scans
4852 - if (count($candidates) > $max_candidates) {
4853 - usort($candidates, function ($a, $b) {
4854 - return $b['similarity'] <=> $a['similarity'];
4855 - });
4856 - $candidates = array_slice($candidates, 0, $max_candidates);
4857 - }
4858 -
4859 - $offset += $batch_size;
4860 - } while (true);
4861 -
4862 - if ($total_checked === 0) {
4863 - $this->current_valid_urls = [];
4864 - return '';
3675 +
3676 + unset($database_embedding);
4865 3677 }
4866 3678
4867 - // Final candidates sort (best first)
4868 - if (count($candidates) > 1) {
4869 - usort($candidates, function ($a, $b) {
4870 - return $b['similarity'] <=> $a['similarity'];
4871 - });
4872 - }
4873 -
4874 - // ===== PHASE 2: FETCH ARTICLE CONTENT ONLY FOR WINNERS =====
4875 - // Gather unique IDs we actually need (top_display + candidates) and pull
4876 - // article_content in bounded IN() batches. This avoids loading content for
4877 - // every row during the similarity scan.
4878 - $needed_ids = [];
4879 - foreach ($top_display as $item) {
4880 - $needed_ids[$item['id']] = true;
4881 - }
4882 - foreach ($candidates as $item) {
4883 - $needed_ids[$item['id']] = true;
4884 - }
4885 - $needed_ids = array_keys($needed_ids);
4886 -
4887 - $content_map = [];
4888 - if (!empty($needed_ids)) {
4889 - foreach (array_chunk($needed_ids, 250) as $chunk_ids) {
4890 - $placeholders = implode(',', array_fill(0, count($chunk_ids), '%d'));
4891 - $rows = $wpdb->get_results($wpdb->prepare(
4892 - "SELECT id, article_content FROM {$system_prompt_table} WHERE id IN ($placeholders)",
4893 - ...$chunk_ids
4894 - ));
4895 - foreach ($rows as $r) {
4896 - $content_map[$r->id] = $r->article_content;
4897 - }
4898 - unset($rows);
4899 - }
4900 - }
4901 -
4902 - // Build the all_similarities display array from the top 10
4903 - $all_similarities = [];
4904 - foreach ($top_display as $item) {
4905 - $article_content_for_parse = $content_map[$item['id']] ?? '';
4906 - $parsed_for_display = MxChat_Chunker::parse_stored_chunk($article_content_for_parse);
4907 - $is_chunk = $parsed_for_display['is_chunked'];
4908 - $chunk_meta = $parsed_for_display['metadata'];
4909 -
4910 - if (!empty($item['source_url']) && $item['source_url'] !== '#') {
4911 - $source_display = $item['source_url'];
4912 - } else {
4913 - $content_preview = strip_tags($article_content_for_parse);
4914 - $content_preview = preg_replace('/\s+/', ' ', $content_preview);
4915 - $source_display = substr(trim($content_preview), 0, 50) . '...';
4916 - }
4917 -
4918 - $all_similarities[] = [
4919 - 'document_id' => $item['id'],
4920 - 'similarity' => $item['similarity'],
4921 - 'similarity_percentage' => round($item['similarity'] * 100, 2),
4922 - 'above_threshold' => $item['similarity'] >= $similarity_threshold,
4923 - 'source_display' => $source_display,
4924 - 'content_preview' => substr(strip_tags($parsed_for_display['text'] ?? ''), 0, 100) . '...',
4925 - 'used_for_context' => false,
4926 - 'role_restriction' => $item['role_restriction'],
4927 - 'has_access' => $item['has_access'],
4928 - 'filtered_out' => !$item['has_access'],
4929 - 'is_chunk' => $is_chunk,
4930 - 'chunk_index' => $is_chunk ? ($chunk_meta['chunk_index'] ?? 0) : null,
4931 - 'total_chunks' => $is_chunk ? ($chunk_meta['total_chunks'] ?? 1) : null
4932 - ];
4933 - }
4934 -
4935 - // Build url_groups from candidates for chunk reassembly
4936 - $url_groups = array();
4937 - foreach ($candidates as $cand) {
4938 - $article_content = $content_map[$cand['id']] ?? '';
4939 - $parsed = MxChat_Chunker::parse_stored_chunk($article_content);
4940 - $is_chunked = $parsed['is_chunked'];
4941 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
4942 - $text_content = $parsed['text'];
4943 -
4944 - $source_url = $cand['source_url'];
4945 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $cand['id'];
4946 -
4947 - if (!isset($url_groups[$group_key])) {
4948 - $url_groups[$group_key] = array(
4949 - 'source_url' => $source_url,
4950 - 'best_score' => 0,
4951 - 'is_chunked' => $is_chunked,
4952 - 'chunks' => array(),
4953 - 'single_text' => '',
4954 - 'single_id' => null
4955 - );
4956 - }
4957 -
4958 - if ($cand['similarity'] > $url_groups[$group_key]['best_score']) {
4959 - $url_groups[$group_key]['best_score'] = $cand['similarity'];
4960 - }
4961 -
4962 - if ($is_chunked) {
4963 - $url_groups[$group_key]['is_chunked'] = true;
4964 - $url_groups[$group_key]['chunks'][] = array(
4965 - 'id' => $cand['id'],
4966 - 'score' => $cand['similarity'],
4967 - 'chunk_index' => $chunk_index,
4968 - 'text' => $text_content
4969 - );
4970 - } else {
4971 - $url_groups[$group_key]['single_text'] = $text_content;
4972 - $url_groups[$group_key]['single_id'] = $cand['id'];
4973 - }
4974 - }
4975 -
4976 3679 // Sort ALL similarities for testing display (highest first)
4977 3680 usort($all_similarities, function ($a, $b) {
4978 3681 return $b['similarity'] <=> $a['similarity'];
4979 3682 });
4980 -
4981 - // Sort URL groups by best score (highest first)
4982 - uasort($url_groups, function($a, $b) {
4983 - 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'];
4984 3687 });
4985 -
4986 - // Get RAG sources limit from options (default 6, min 3, max 10)
4987 - $rag_sources_limit = isset($options['rag_sources_limit']) ? intval($options['rag_sources_limit']) : 3;
4988 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
4989 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
4990 -
4991 - // Take top N unique URLs based on user setting
4992 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
4993 -
4994 - // 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
4995 3693 $used_document_ids = [];
4996 - foreach ($top_urls as $group) {
4997 - if ($group['is_chunked']) {
4998 - foreach ($group['chunks'] as $chunk) {
4999 - $used_document_ids[] = $chunk['id'];
5000 - }
5001 - } elseif ($group['single_id']) {
5002 - $used_document_ids[] = $group['single_id'];
5003 - }
3694 + foreach ($top_results as $result) {
3695 + $used_document_ids[] = $result['id'];
5004 3696 }
5005 -
3697 +
5006 3698 // Update the all_similarities array to mark which were actually used
5007 3699 foreach ($all_similarities as &$similarity_item) {
5008 3700 $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
5009 3701 }
5010 -
5011 - // Store top 10 for testing panel
3702 +
3703 + // Store top 10 for testing panel (now with correct used_for_context flags and role info)
5012 3704 $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
5013 - $this->last_similarity_analysis['total_checked'] = $total_checked;
5014 -
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 +
5015 3709 // Initialize final content
5016 3710 $content = '';
5017 - $matches_used = 0;
5018 - $total_chunks_used = 0;
5019 - $max_total_chunks = isset($options['rag_chunks_limit']) ? intval($options['rag_chunks_limit']) : 15;
5020 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5021 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5022 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5023 -
5024 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5025 - // Use fresh options to ensure we get the latest setting value
5026 - $fresh_options = get_option('mxchat_options', []);
5027 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5028 -
5029 - // Build content from top sources
5030 - foreach ($top_urls as $group_key => $group) {
5031 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5032 -
5033 - // Stop if we've hit the total chunk limit
5034 - if ($total_chunks_used >= $max_total_chunks) {
5035 - 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;
5036 3719 }
5037 -
5038 - $full_text = '';
5039 - $chunks_in_this_source = 1; // Default for non-chunked content
5040 -
5041 - if ($group['is_chunked']) {
5042 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5043 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5044 -
5045 - // Fetch chunks for this URL with limit
5046 - $full_text = $this->reassemble_chunks_from_wordpress($source_url, $chunks_remaining, $chunks_in_this_source);
5047 -
5048 - // If fetching all chunks fails, fall back to matched chunks
5049 - if (empty($full_text)) {
5050 - // Sort matched chunks by index and concatenate
5051 - usort($group['chunks'], function($a, $b) {
5052 - return $a['chunk_index'] <=> $b['chunk_index'];
5053 - });
5054 -
5055 - $chunk_texts = array();
5056 - $chunks_in_this_source = 0;
5057 - foreach ($group['chunks'] as $chunk) {
5058 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5059 - break;
5060 - }
5061 - $chunk_texts[] = $chunk['text'];
5062 - $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;
5063 3746 }
5064 - $full_text = implode("\n\n", $chunk_texts);
5065 3747 }
5066 - } else {
5067 - $full_text = $group['single_text'];
5068 - $chunks_in_this_source = 1;
5069 - }
5070 -
5071 - if (!empty($full_text)) {
5072 - // Strip URLs from content if citation links are disabled
5073 - if (!$citation_links_enabled) {
5074 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5075 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
5076 - }
5077 -
5078 - // Use numbered reference for URL-based entries, plain info label for manual entries
5079 - if (!empty($source_url) && $source_url !== '#') {
5080 - $matches_used++;
5081 - $content .= "## Reference " . $matches_used . " ##\n";
5082 - $content .= $full_text . "\n\n";
5083 -
5084 - // Only include citation URLs if citation links are enabled
5085 - if ($citation_links_enabled) {
5086 - $valid_urls[] = $source_url;
5087 - $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;
5088 3755 }
5089 - } else {
5090 - // Manual entry — no reference number, no citation
5091 - $content .= "## Information ##\n";
5092 - $content .= $full_text . "\n\n";
5093 3756 }
5094 -
5095 - // Extract any URLs from the text content itself (only if citation links enabled)
5096 - if ($citation_links_enabled) {
5097 - preg_match_all(
5098 - '#\bhttps?://[^\s<>"\']+#i',
5099 - $full_text,
5100 - $content_urls
5101 - );
5102 - if (!empty($content_urls[0])) {
5103 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5104 - }
5105 - }
5106 -
5107 - $total_chunks_used += $chunks_in_this_source;
5108 3757 }
5109 3758 }
5110 -
5111 - // NEW: Store unique valid URLs for validation
5112 - $this->current_valid_urls = array_unique($valid_urls);
5113 -
5114 - // Store sources and chunks counts for testing/transcript display
5115 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5116 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5117 -
5118 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5119 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5120 -
3759 +
5121 3760 // Add response guidelines
5122 - if (empty($top_urls)) {
3761 + if (empty($top_results)) {
5123 3762 $content = "No reference information was found for this query.\n\n";
5124 3763 } else {
5125 - // Build response guidelines based on citation links setting
5126 3764 $content .= "\n## Response Guidelines ##\n" .
5127 3765 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5128 3766 "Be conversational and friendly, but never mention your knowledge base or training data. " .
5129 3767 "If you don't have specific information or are uncertain about any details, it's always " .
5130 3768 "better to honestly say you don't know rather than making up or guessing at answers. " .
5131 - "When information is incomplete, let them know you are unsure.\n\n";
5132 -
5133 - // Only add hyperlink instructions if citation links are enabled
5134 - if ($citation_links_enabled) {
5135 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5136 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5137 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
5138 - } else {
5139 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5140 - "Simply provide helpful answers based on the reference information without citing sources.";
5141 - }
3769 + "When information is incomplete, let them know you are unsure.";
5142 3770 }
5143 3771
5144 3772 return trim($content);
5145 3773 }
5146 3774
5147 -/**
5148 - * Fetch and reassemble chunks for a URL from WordPress database
5149 - *
5150 - * @param string $source_url The source URL to fetch chunks for
5151 - * @param int $max_chunks Maximum number of chunks to return (0 = unlimited)
5152 - * @param int &$chunk_count Reference to store the actual number of chunks returned
5153 - * @return string Reassembled content from chunks
5154 - */
5155 -private function reassemble_chunks_from_wordpress($source_url, $max_chunks = 0, &$chunk_count = 0) {
5156 - global $wpdb;
5157 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
5158 -
5159 - // Fetch all rows with this source_url
5160 - $rows = $wpdb->get_results($wpdb->prepare(
5161 - "SELECT article_content FROM {$table}
5162 - WHERE source_url = %s
5163 - ORDER BY id ASC",
5164 - $source_url
5165 - ));
5166 -
5167 - if (empty($rows)) {
5168 - $chunk_count = 0;
5169 - return '';
5170 - }
5171 -
5172 - // Parse and sort chunks by index
5173 - $chunks = array();
5174 - foreach ($rows as $row) {
5175 - $parsed = MxChat_Chunker::parse_stored_chunk($row->article_content);
5176 -
5177 - if ($parsed['is_chunked']) {
5178 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
5179 - $chunks[$chunk_index] = $parsed['text'];
5180 - } else {
5181 - // Non-chunked content - just return it
5182 - $chunks[] = $parsed['text'];
5183 - }
5184 - }
5185 -
5186 - // Sort by chunk index
5187 - ksort($chunks);
5188 -
5189 - // Apply chunk limit if specified
5190 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5191 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5192 - }
5193 -
5194 - // Store actual chunk count
5195 - $chunk_count = count($chunks);
5196 -
5197 - // Reassemble content
5198 - return implode("\n\n", $chunks);
5199 -}
5200 -
5201 3775 private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
5202 3776 global $wpdb;
5203 3777
5204 - //error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
5205 - //error_log(" - bot_id: " . $bot_id);
5206 - //error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
5207 - //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'));
5208 3782
5209 3783 // Use bot-specific config or fall back to default
5210 3784 if ($bot_config === null) {
5211 3785 $bot_config = $this->get_bot_pinecone_config($bot_id);
@@ -5214,12 +3788,12 @@
5214 3788 $api_key = $bot_config['api_key'] ?? '';
5215 3789 $host = $bot_config['host'] ?? '';
5216 3790 $namespace = $bot_config['namespace'] ?? '';
5217 3791
5218 - //error_log("MXCHAT DEBUG: Pinecone query parameters:");
5219 - //error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
5220 - //error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
5221 - //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));
5222 3796
5223 3797 // Initialize similarity analysis storage
5224 3798 $this->last_similarity_analysis = [
5225 3799 'knowledge_base_type' => 'Pinecone',
@@ -5229,17 +3803,12 @@
5229 3803 'threshold_used' => 0,
5230 3804 'total_checked' => 0
5231 3805 ];
5232 3806
5233 - // NEW: Initialize valid URLs array
5234 - $valid_urls = [];
5235 -
5236 3807 if (empty($host) || empty($api_key)) {
5237 - //error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
5238 - //error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
5239 - //error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
5240 - // Store empty array for valid URLs since we can't proceed
5241 - $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'));
5242 3811 return '';
5243 3812 }
5244 3813
5245 3814 // Get knowledge manager instance for role checking
@@ -5259,9 +3828,9 @@
5259 3828 $api_endpoint = "https://{$host}/query";
5260 3829
5261 3830 $request_body = array(
5262 3831 'vector' => $user_embedding,
5263 - '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
5264 3833 'includeMetadata' => true,
5265 3834 'includeValues' => true
5266 3835 );
5267 3836
@@ -5269,11 +3838,11 @@
5269 3838 if (!empty($namespace)) {
5270 3839 $request_body['namespace'] = $namespace;
5271 3840 }
5272 3841
5273 - //error_log("MXCHAT DEBUG: About to call Pinecone API");
5274 - //error_log(" - Endpoint: " . $api_endpoint);
5275 - //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'));
5276 3845
5277 3846 $response = wp_remote_post($api_endpoint, array(
5278 3847 'headers' => array(
5279 3848 'Api-Key' => $api_key,
@@ -5284,61 +3853,53 @@
5284 3853 'timeout' => 30
5285 3854 ));
5286 3855
5287 3856 if (is_wp_error($response)) {
5288 - //error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
5289 - // Store empty array for valid URLs
5290 - $this->current_valid_urls = [];
3857 + error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
5291 3858 return '';
5292 3859 }
5293 3860
5294 3861 $response_code = wp_remote_retrieve_response_code($response);
5295 - //error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
3862 + error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
5296 3863
5297 3864 if ($response_code !== 200) {
5298 3865 $response_body = wp_remote_retrieve_body($response);
5299 - //error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
5300 - // Store empty array for valid URLs
5301 - $this->current_valid_urls = [];
3866 + error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
5302 3867 return '';
5303 3868 }
5304 3869
5305 3870 // ADD DETAILED DEBUG SECTION HERE
5306 3871 $response_body = wp_remote_retrieve_body($response);
5307 - //error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
3872 + error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
5308 3873
5309 3874 $results = json_decode($response_body, true);
5310 3875
5311 3876 if (json_last_error() !== JSON_ERROR_NONE) {
5312 - //error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
5313 - //error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
5314 - // Store empty array for valid URLs
5315 - $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));
5316 3879 return '';
5317 3880 }
5318 3881
5319 - //error_log("MXCHAT DEBUG: Pinecone response structure:");
5320 - //error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
5321 - //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'));
5322 3885
5323 3886 if (empty($results['matches'])) {
5324 - //error_log("MXCHAT DEBUG: No matches found in Pinecone response");
5325 - //error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
5326 - // Store empty array for valid URLs
5327 - $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)));
5328 3889 return '';
5329 3890 }
5330 3891
5331 - //error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
3892 + error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
5332 3893
5333 3894 // Log first match details for debugging
5334 3895 if (!empty($results['matches'][0])) {
5335 3896 $first_match = $results['matches'][0];
5336 - //error_log("MXCHAT DEBUG: First match details:");
5337 - //error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
5338 - //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'));
5339 3900 if (isset($first_match['metadata'])) {
5340 - //error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
3901 + error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
5341 3902 }
5342 3903 }
5343 3904
5344 3905 // Initialize the final content
@@ -5344,181 +3905,45 @@
5344 3905 // Initialize the final content
5345 3906 $content = '';
5346 3907 $matches_used = 0;
5347 3908 $matches_used_for_context = [];
5348 - $total_chunks_used = 0;
5349 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5350 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5351 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5352 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5353 -
5354 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5355 - // Use fresh options to ensure we get the latest setting value
5356 - $fresh_options = get_option('mxchat_options', []);
5357 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5358 -
5359 - // NEW CHUNKING LOGIC: Group results by source_url for chunk reassembly
5360 - $url_groups = array();
5361 -
3909 +
3910 + // Process each match for actual content generation (lazy role checking)
5362 3911 foreach ($results['matches'] as $index => $match) {
5363 3912 // Skip if similarity is below threshold
5364 3913 if ($match['score'] < $similarity_threshold) {
5365 3914 continue;
5366 3915 }
5367 -
5368 - $metadata = $match['metadata'] ?? array();
5369 - $source_url = $metadata['source_url'] ?? '';
5370 - $match_id = $match['id'] ?? '';
5371 -
5372 - // LAZY ROLE CHECK: Only check role for content we're actually considering
5373 - $role_restriction = $this->get_single_vector_role($match_id, $metadata);
5374 - $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
5375 -
5376 - // Skip if user doesn't have access
5377 - if (!$has_access) {
5378 - continue;
5379 - }
5380 -
5381 - // Use a unique key for manual entries without a source URL
5382 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $match_id;
5383 -
5384 - // Group by source URL (or unique key for manual entries)
5385 - if (!isset($url_groups[$group_key])) {
5386 - $url_groups[$group_key] = array(
5387 - 'source_url' => $source_url,
5388 - 'best_score' => 0,
5389 - 'is_chunked' => isset($metadata['is_chunked']) && $metadata['is_chunked'],
5390 - 'chunks' => array(),
5391 - 'single_text' => ''
5392 - );
5393 - }
5394 -
5395 - // Track best score for this group
5396 - if ($match['score'] > $url_groups[$group_key]['best_score']) {
5397 - $url_groups[$group_key]['best_score'] = $match['score'];
5398 - }
5399 -
5400 - // Store chunk info or single text
5401 - if ($url_groups[$group_key]['is_chunked']) {
5402 - $url_groups[$group_key]['chunks'][] = array(
5403 - 'id' => $match_id,
5404 - 'score' => $match['score'],
5405 - 'chunk_index' => $metadata['chunk_index'] ?? 0,
5406 - 'text' => $metadata['text'] ?? ''
5407 - );
5408 - } else {
5409 - // Non-chunked content - just store the text
5410 - $url_groups[$group_key]['single_text'] = $metadata['text'] ?? '';
5411 - $url_groups[$group_key]['single_id'] = $match_id;
5412 - }
5413 - }
5414 -
5415 - // Sort URL groups by best score (highest first)
5416 - uasort($url_groups, function($a, $b) {
5417 - return $b['best_score'] <=> $a['best_score'];
5418 - });
5419 -
5420 - // Get RAG sources limit from options (default 6, min 3, max 10)
5421 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
5422 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
5423 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
5424 -
5425 - // Take top N unique URLs based on user setting
5426 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5427 -
5428 - // Track which match IDs are actually used for context
5429 - foreach ($top_urls as $group) {
5430 - if ($group['is_chunked']) {
5431 - foreach ($group['chunks'] as $chunk) {
5432 - $matches_used_for_context[] = $chunk['id'];
5433 - }
5434 - } elseif (!empty($group['single_id'])) {
5435 - $matches_used_for_context[] = $group['single_id'];
5436 - }
5437 - }
5438 -
5439 - // Build content from top sources
5440 - foreach ($top_urls as $group_key => $group) {
5441 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5442 -
5443 - // Stop if we've hit the total chunk limit
5444 - if ($total_chunks_used >= $max_total_chunks) {
3916 +
3917 + // Limit to top 5 matches above threshold
3918 + if ($matches_used >= 5) {
5445 3919 break;
5446 3920 }
5447 -
5448 - $full_text = '';
5449 - $chunks_in_this_source = 1; // Default for non-chunked content
5450 -
5451 - if ($group['is_chunked']) {
5452 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5453 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5454 -
5455 - // Fetch chunks for this URL with limit
5456 - $full_text = $this->reassemble_chunks_from_pinecone($source_url, $bot_config, $chunks_remaining, $chunks_in_this_source);
5457 -
5458 - // If fetching all chunks fails, fall back to matched chunks
5459 - if (empty($full_text)) {
5460 - // Sort matched chunks by index and concatenate
5461 - usort($group['chunks'], function($a, $b) {
5462 - return $a['chunk_index'] <=> $b['chunk_index'];
5463 - });
5464 -
5465 - $chunk_texts = array();
5466 - $chunks_in_this_source = 0;
5467 - foreach ($group['chunks'] as $chunk) {
5468 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5469 - break;
5470 - }
5471 - $chunk_texts[] = $chunk['text'];
5472 - $chunks_in_this_source++;
5473 - }
5474 - $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;
5475 3931 }
5476 - } else {
5477 - $full_text = $group['single_text'];
5478 - $chunks_in_this_source = 1;
5479 - }
5480 -
5481 - if (!empty($full_text)) {
5482 - // Strip URLs from content if citation links are disabled
5483 - if (!$citation_links_enabled) {
5484 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5485 - $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";
5486 3939 }
5487 -
5488 - // Use numbered reference for URL-based entries, plain info label for manual entries
5489 - if (!empty($source_url) && $source_url !== '#') {
5490 - $matches_used++;
5491 - $content .= "## Reference " . $matches_used . " ##\n";
5492 - $content .= $full_text . "\n\n";
5493 -
5494 - // Only include citation URLs if citation links are enabled
5495 - if ($citation_links_enabled) {
5496 - $valid_urls[] = $source_url;
5497 - $content .= "URL: " . $source_url . "\n\n";
5498 - }
5499 - } else {
5500 - // Manual entry — no reference number, no citation
5501 - $content .= "## Information ##\n";
5502 - $content .= $full_text . "\n\n";
5503 - }
5504 -
5505 - // Extract any URLs from the text content itself (only if citation links enabled)
5506 - if ($citation_links_enabled) {
5507 - preg_match_all(
5508 - '#\bhttps?://[^\s<>"\']+#i',
5509 - $full_text,
5510 - $content_urls
5511 - );
5512 - if (!empty($content_urls[0])) {
5513 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5514 - }
5515 - }
5516 -
5517 - $total_chunks_used += $chunks_in_this_source;
3940 +
3941 + $matches_used_for_context[] = $match['id'] ?? $index;
3942 + $matches_used++;
5518 3943 }
5519 3944 }
5520 -
3945 +
5521 3946 // Process ALL matches for testing data (top 10) - with role checking for testing display
5522 3947 $all_matches = [];
5523 3948 foreach ($results['matches'] as $index => $match) {
5524 3949 if ($index >= 10) break; // Limit to top 10 for testing
@@ -5538,19 +3963,9 @@
5538 3963 $source_display = substr(trim($content_preview), 0, 50) . '...';
5539 3964 }
5540 3965
5541 3966 $match_id_for_display = $match['id'] ?? $index;
5542 -
5543 - // Check for chunk metadata in Pinecone
5544 - $is_chunk = isset($match['metadata']['is_chunked']) && $match['metadata']['is_chunked'];
5545 - $chunk_index = isset($match['metadata']['chunk_index']) ? intval($match['metadata']['chunk_index']) : null;
5546 - $total_chunks = isset($match['metadata']['total_chunks']) ? intval($match['metadata']['total_chunks']) : null;
5547 -
5548 - // Also detect chunk from vector ID pattern: {hash}_chunk_{index}
5549 - if (!$is_chunk && MxChat_Chunker::is_chunk_vector_id($match_id_for_display)) {
5550 - $is_chunk = true;
5551 - }
5552 -
3967 +
5553 3968 $all_matches[] = [
5554 3969 'document_id' => $match_id_for_display,
5555 3970 'similarity' => $match['score'],
5556 3971 'similarity_percentage' => round($match['score'] * 100, 2),
@@ -5559,12 +3974,9 @@
5559 3974 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
5560 3975 'used_for_context' => in_array($match_id_for_display, $matches_used_for_context),
5561 3976 'role_restriction' => $role_restriction,
5562 3977 'has_access' => $has_access,
5563 - 'filtered_out' => !$has_access,
5564 - 'is_chunk' => $is_chunk,
5565 - 'chunk_index' => $chunk_index,
5566 - 'total_chunks' => $total_chunks
3978 + 'filtered_out' => !$has_access
5567 3979 ];
5568 3980 }
5569 3981
5570 3982 // Store for testing panel
@@ -5569,43 +3981,25 @@
5569 3981
5570 3982 // Store for testing panel
5571 3983 $this->last_similarity_analysis['top_matches'] = $all_matches;
5572 3984 $this->last_similarity_analysis['total_checked'] = count($results['matches']);
5573 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5574 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5575 -
5576 - // NEW: Store unique valid URLs for validation
5577 - $this->current_valid_urls = array_unique($valid_urls);
5578 -
5579 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5580 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5581 -
3985 +
5582 3986 // Add response guidelines
5583 3987 if ($matches_used === 0) {
5584 3988 $content = "No reference information was found for this query.\n\n";
5585 3989 } else {
5586 - // Build response guidelines based on citation links setting
5587 3990 $content .= "\n## Response Guidelines ##\n" .
5588 3991 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5589 3992 "Be conversational and friendly, but never mention your knowledge base or training data. " .
5590 3993 "If you don't have specific information or are uncertain about any details, it's always " .
5591 3994 "better to honestly say you don't know rather than making up or guessing at answers. " .
5592 - "When information is incomplete, let them know you are unsure.\n\n";
5593 -
5594 - // Only add hyperlink instructions if citation links are enabled
5595 - if ($citation_links_enabled) {
5596 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5597 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5598 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
5599 - } else {
5600 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5601 - "Simply provide helpful answers based on the reference information without citing sources.";
5602 - }
3995 + "When information is incomplete, let them know you are unsure.";
5603 3996 }
5604 -
3997 +
5605 3998 return trim($content);
5606 3999 }
5607 4000
4001 +
5608 4002 /**
5609 4003 * Get role restriction for a single vector (with caching)
5610 4004 */
5611 4005 private function get_single_vector_role($vector_id, $metadata = array()) {
@@ -5642,518 +4036,12 @@
5642 4036 }
5643 4037
5644 4038 // Cache individual role for 1 hour
5645 4039 wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
5646 -
4040 +
5647 4041 return $role_restriction;
5648 4042 }
5649 4043
5650 -/**
5651 - * Fetch and reassemble all chunks for a URL from Pinecone
5652 - *
5653 - * @param string $source_url The source URL to fetch chunks for
5654 - * @param array $bot_config Bot-specific Pinecone configuration
5655 - * @return string Reassembled content from all chunks
5656 - */
5657 -private function reassemble_chunks_from_pinecone($source_url, $bot_config, $max_chunks = 0, &$chunk_count = 0) {
5658 - $api_key = $bot_config['api_key'] ?? '';
5659 - $host = $bot_config['host'] ?? '';
5660 - $namespace = $bot_config['namespace'] ?? '';
5661 -
5662 - if (empty($host) || empty($api_key)) {
5663 - $chunk_count = 0;
5664 - return '';
5665 - }
5666 -
5667 - $base_hash = md5($source_url);
5668 -
5669 - // Use Pinecone list API to find all chunk vectors with this prefix
5670 - $list_url = "https://{$host}/vectors/list";
5671 -
5672 - // Limit to max_chunks if specified, otherwise fetch up to 100
5673 - $fetch_limit = ($max_chunks > 0 && $max_chunks < 100) ? $max_chunks : 100;
5674 -
5675 - $list_body = array(
5676 - 'prefix' => $base_hash . '_chunk_',
5677 - 'limit' => $fetch_limit
5678 - );
5679 -
5680 - if (!empty($namespace)) {
5681 - $list_body['namespace'] = $namespace;
5682 - }
5683 -
5684 - $list_response = wp_remote_post($list_url, array(
5685 - 'headers' => array(
5686 - 'Api-Key' => $api_key,
5687 - 'accept' => 'application/json',
5688 - 'content-type' => 'application/json'
5689 - ),
5690 - 'body' => wp_json_encode($list_body),
5691 - 'timeout' => 30
5692 - ));
5693 -
5694 - if (is_wp_error($list_response)) {
5695 - //error_log('[MXCHAT-CHUNK] List API error: ' . $list_response->get_error_message());
5696 - return '';
5697 - }
5698 -
5699 - $list_data = json_decode(wp_remote_retrieve_body($list_response), true);
5700 -
5701 - if (empty($list_data['vectors'])) {
5702 - //error_log('[MXCHAT-CHUNK] No chunk vectors found for URL: ' . $source_url);
5703 - return '';
5704 - }
5705 -
5706 - // Extract vector IDs
5707 - $vector_ids = array();
5708 - foreach ($list_data['vectors'] as $vector) {
5709 - if (isset($vector['id'])) {
5710 - $vector_ids[] = $vector['id'];
5711 - }
5712 - }
5713 -
5714 - if (empty($vector_ids)) {
5715 - return '';
5716 - }
5717 -
5718 - // Fetch all chunk content
5719 - $fetch_url = "https://{$host}/vectors/fetch";
5720 -
5721 - $fetch_body = array(
5722 - 'ids' => $vector_ids
5723 - );
5724 -
5725 - if (!empty($namespace)) {
5726 - $fetch_body['namespace'] = $namespace;
5727 - }
5728 -
5729 - $fetch_response = wp_remote_post($fetch_url, array(
5730 - 'headers' => array(
5731 - 'Api-Key' => $api_key,
5732 - 'accept' => 'application/json',
5733 - 'content-type' => 'application/json'
5734 - ),
5735 - 'body' => wp_json_encode($fetch_body),
5736 - 'timeout' => 30
5737 - ));
5738 -
5739 - if (is_wp_error($fetch_response)) {
5740 - //error_log('[MXCHAT-CHUNK] Fetch API error: ' . $fetch_response->get_error_message());
5741 - return '';
5742 - }
5743 -
5744 - $fetch_data = json_decode(wp_remote_retrieve_body($fetch_response), true);
5745 -
5746 - if (empty($fetch_data['vectors'])) {
5747 - return '';
5748 - }
5749 -
5750 - // Sort chunks by index and reassemble
5751 - $chunks = array();
5752 - foreach ($fetch_data['vectors'] as $id => $vector) {
5753 - $metadata = $vector['metadata'] ?? array();
5754 - $chunk_index = $metadata['chunk_index'] ?? 0;
5755 - $text = $metadata['text'] ?? '';
5756 -
5757 - // Store chunk with its index
5758 - $chunks[$chunk_index] = $text;
5759 - }
5760 -
5761 - // Sort by chunk index
5762 - ksort($chunks);
5763 -
5764 - // Apply chunk limit if specified
5765 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5766 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5767 - }
5768 -
5769 - // Store actual chunk count
5770 - $chunk_count = count($chunks);
5771 -
5772 - // Reassemble content
5773 - return implode("\n\n", $chunks);
5774 -}
5775 -
5776 -/**
5777 - * Search for relevant content using OpenAI Vector Store (File Search)
5778 - *
5779 - * @param string $user_query The user's query text
5780 - * @param string $bot_id The bot ID
5781 - * @param array $vectorstore_config Vector Store configuration
5782 - * @return string Formatted context string with references
5783 - */
5784 -private function find_relevant_content_openai_vectorstore($user_query, $bot_id = 'default', $vectorstore_config = array()) {
5785 - //error_log("MXCHAT DEBUG: find_relevant_content_openai_vectorstore called");
5786 - //error_log(" - bot_id: " . $bot_id);
5787 - //error_log(" - user_query length: " . strlen($user_query));
5788 -
5789 - // Get OpenAI API key
5790 - $mxchat_options = get_option('mxchat_options', array());
5791 - $api_key = $mxchat_options['api_key'] ?? '';
5792 -
5793 - // Reset vectorstore error tracking
5794 - $this->last_vectorstore_error = null;
5795 -
5796 - if (empty($api_key)) {
5797 - //error_log("MXCHAT DEBUG ERROR: OpenAI API key not configured");
5798 - $this->last_vectorstore_error = 'Vector Store search failed: OpenAI API key is not configured.';
5799 - $this->current_valid_urls = [];
5800 - return '';
5801 - }
5802 -
5803 - // Get Vector Store configuration
5804 - if (empty($vectorstore_config)) {
5805 - $vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
5806 - }
5807 -
5808 - $vectorstore_ids_string = $vectorstore_config['vectorstore_ids'] ?? '';
5809 - $max_results = $vectorstore_config['max_results'] ?? 5;
5810 -
5811 - if (empty($vectorstore_ids_string)) {
5812 - //error_log("MXCHAT DEBUG ERROR: No Vector Store IDs configured");
5813 - $this->last_vectorstore_error = 'Vector Store search failed: No Vector Store IDs are configured for this bot.';
5814 - $this->current_valid_urls = [];
5815 - return '';
5816 - }
5817 -
5818 - // Parse Vector Store IDs
5819 - $vectorstore_ids = array_map('trim', explode(',', $vectorstore_ids_string));
5820 - $vectorstore_ids = array_filter($vectorstore_ids); // Remove empty values
5821 -
5822 - //error_log("MXCHAT DEBUG: Vector Store IDs: " . implode(', ', $vectorstore_ids));
5823 - //error_log("MXCHAT DEBUG: Max results: " . $max_results);
5824 -
5825 - // Initialize similarity analysis storage
5826 - $this->last_similarity_analysis = [
5827 - 'knowledge_base_type' => 'OpenAI Vector Store',
5828 - 'bot_id' => $bot_id,
5829 - 'vectorstore_ids' => $vectorstore_ids,
5830 - 'top_matches' => [],
5831 - 'threshold_used' => 0,
5832 - 'total_checked' => 0
5833 - ];
5834 -
5835 - $valid_urls = [];
5836 -
5837 - // Get the selected model
5838 - $bot_options = $this->get_bot_options($bot_id);
5839 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
5840 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
5841 -
5842 - // Verify it's an OpenAI model
5843 - if (!$this->is_openai_chat_model($selected_model)) {
5844 - //error_log("MXCHAT DEBUG ERROR: Vector Store search requires OpenAI model. Current: " . $selected_model);
5845 - $this->last_vectorstore_error = 'Vector Store search requires an OpenAI model. Current model: ' . $selected_model;
5846 - $this->current_valid_urls = [];
5847 - return '';
5848 - }
5849 -
5850 - // Use OpenAI Responses API with file_search tool
5851 - $request_body = array(
5852 - 'model' => $selected_model,
5853 - 'input' => $user_query,
5854 - 'tools' => array(
5855 - array(
5856 - 'type' => 'file_search',
5857 - 'vector_store_ids' => $vectorstore_ids,
5858 - 'max_num_results' => intval($max_results)
5859 - )
5860 - ),
5861 - 'include' => array('output[*].file_search_call.search_results')
5862 - );
5863 -
5864 - //error_log("MXCHAT VECTORSTORE: ========== REQUEST START ==========");
5865 - //error_log("MXCHAT VECTORSTORE: Model: " . $selected_model);
5866 - //error_log("MXCHAT VECTORSTORE: Query: " . substr($user_query, 0, 200));
5867 - //error_log("MXCHAT VECTORSTORE: Vector Store IDs: " . implode(', ', $vectorstore_ids));
5868 - //error_log("MXCHAT VECTORSTORE: Max Results: " . $max_results);
5869 - //error_log("MXCHAT VECTORSTORE: Request body: " . wp_json_encode($request_body));
5870 -
5871 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
5872 - 'headers' => array(
5873 - 'Authorization' => 'Bearer ' . $api_key,
5874 - 'Content-Type' => 'application/json'
5875 - ),
5876 - 'body' => wp_json_encode($request_body),
5877 - 'timeout' => 60
5878 - ));
5879 -
5880 - if (is_wp_error($response)) {
5881 - //error_log("MXCHAT VECTORSTORE ERROR: WP Error: " . $response->get_error_message());
5882 - $this->last_vectorstore_error = 'Vector Store API request failed: ' . $response->get_error_message();
5883 - $this->current_valid_urls = [];
5884 - return '';
5885 - }
5886 -
5887 - $response_code = wp_remote_retrieve_response_code($response);
5888 - //error_log("MXCHAT VECTORSTORE: Response code: " . $response_code);
5889 -
5890 - $response_body = wp_remote_retrieve_body($response);
5891 - //error_log("MXCHAT VECTORSTORE: Raw response (first 2000 chars): " . substr($response_body, 0, 2000));
5892 -
5893 - if ($response_code !== 200) {
5894 - //error_log("MXCHAT VECTORSTORE ERROR: API error response: " . $response_body);
5895 - $api_error_detail = '';
5896 - $decoded_error = json_decode($response_body, true);
5897 - if (isset($decoded_error['error']['message'])) {
5898 - $api_error_detail = $decoded_error['error']['message'];
5899 - }
5900 - $this->last_vectorstore_error = 'Vector Store API returned HTTP ' . $response_code . ($api_error_detail ? ': ' . $api_error_detail : '');
5901 - $this->current_valid_urls = [];
5902 - return '';
5903 - }
5904 - $result = json_decode($response_body, true);
5905 -
5906 - if (json_last_error() !== JSON_ERROR_NONE) {
5907 - //error_log("MXCHAT VECTORSTORE ERROR: JSON decode error: " . json_last_error_msg());
5908 - $this->last_vectorstore_error = 'Vector Store response could not be parsed: ' . json_last_error_msg();
5909 - $this->current_valid_urls = [];
5910 - return '';
5911 - }
5912 -
5913 - // Debug: Log the structure of the result
5914 - //error_log("MXCHAT VECTORSTORE: Result keys: " . implode(', ', array_keys($result)));
5915 - if (isset($result['output'])) {
5916 - //error_log("MXCHAT VECTORSTORE: Output count: " . count($result['output']));
5917 - foreach ($result['output'] as $idx => $out) {
5918 - //error_log("MXCHAT VECTORSTORE: Output[$idx] type: " . ($out['type'] ?? 'unknown'));
5919 - //error_log("MXCHAT VECTORSTORE: Output[$idx] keys: " . implode(', ', array_keys($out)));
5920 - }
5921 - } else {
5922 - //error_log("MXCHAT VECTORSTORE: No 'output' key in result!");
5923 - }
5924 -
5925 - // Extract file search results from the response
5926 - $content = '';
5927 - $matches_used = 0;
5928 - $all_matches = [];
5929 -
5930 - // The Responses API returns output array with tool results
5931 - if (isset($result['output']) && is_array($result['output'])) {
5932 - foreach ($result['output'] as $output_item) {
5933 - // Look for file_search_call results
5934 - if (isset($output_item['type']) && $output_item['type'] === 'file_search_call') {
5935 - //error_log("MXCHAT VECTORSTORE: Found file_search_call output item");
5936 - //error_log("MXCHAT VECTORSTORE: file_search_call keys: " . implode(', ', array_keys($output_item)));
5937 -
5938 - // Check for search_results in the output item directly
5939 - $search_results = $output_item['search_results'] ?? $output_item['results'] ?? [];
5940 - //error_log("MXCHAT VECTORSTORE: Search results count: " . count($search_results));
5941 -
5942 - if (empty($search_results)) {
5943 - //error_log("MXCHAT VECTORSTORE: No search results found in file_search_call");
5944 - //error_log("MXCHAT VECTORSTORE: file_search_call content: " . wp_json_encode($output_item));
5945 - }
5946 -
5947 - foreach ($search_results as $index => $search_result) {
5948 - $filename = $search_result['filename'] ?? '';
5949 - $score = $search_result['score'] ?? 0;
5950 - $text_content = '';
5951 -
5952 - // Extract text content from the result
5953 - // The text can be directly on the result OR nested under content array
5954 - if (isset($search_result['text']) && !empty($search_result['text'])) {
5955 - // Direct text field (OpenAI's actual format)
5956 - $text_content = $search_result['text'];
5957 - //error_log("MXCHAT VECTORSTORE: Found text directly on result[$index], length: " . strlen($text_content));
5958 - } elseif (isset($search_result['content']) && is_array($search_result['content'])) {
5959 - // Nested content array format
5960 - foreach ($search_result['content'] as $content_item) {
5961 - if (isset($content_item['text'])) {
5962 - $text_content .= $content_item['text'] . "\n";
5963 - }
5964 - }
5965 - //error_log("MXCHAT VECTORSTORE: Found text in content array for result[$index], length: " . strlen($text_content));
5966 - } else {
5967 - //error_log("MXCHAT VECTORSTORE: No text found for result[$index]. Keys: " . implode(', ', array_keys($search_result)));
5968 - }
5969 -
5970 - if (!empty($text_content)) {
5971 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
5972 - $content .= trim($text_content) . "\n\n";
5973 -
5974 - if (!empty($filename)) {
5975 - $content .= "Source: " . $filename . "\n\n";
5976 - }
5977 -
5978 - // Extract URLs from content
5979 - preg_match_all(
5980 - '#\bhttps?://[^\s<>"\']+#i',
5981 - $text_content,
5982 - $content_urls
5983 - );
5984 - if (!empty($content_urls[0])) {
5985 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5986 - }
5987 -
5988 - $matches_used++;
5989 - }
5990 -
5991 - // Store for similarity analysis
5992 - $all_matches[] = [
5993 - 'document_id' => $filename ?: ('result_' . $index),
5994 - 'similarity' => $score,
5995 - 'similarity_percentage' => round($score * 100, 2),
5996 - 'above_threshold' => true,
5997 - 'source_display' => $filename,
5998 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
5999 - 'used_for_context' => true,
6000 - 'role_restriction' => 'public',
6001 - 'has_access' => true,
6002 - 'filtered_out' => false
6003 - ];
6004 - }
6005 - }
6006 -
6007 - // Also check for message content with annotations (citations)
6008 - if (isset($output_item['type']) && $output_item['type'] === 'message') {
6009 - if (isset($output_item['content']) && is_array($output_item['content'])) {
6010 - foreach ($output_item['content'] as $content_block) {
6011 - if (isset($content_block['annotations']) && is_array($content_block['annotations'])) {
6012 - foreach ($content_block['annotations'] as $annotation) {
6013 - if (isset($annotation['filename'])) {
6014 - $filename = $annotation['filename'];
6015 - $score = $annotation['score'] ?? 0;
6016 - $text_content = '';
6017 -
6018 - if (isset($annotation['content']) && is_array($annotation['content'])) {
6019 - foreach ($annotation['content'] as $ann_content) {
6020 - if (isset($ann_content['text'])) {
6021 - $text_content .= $ann_content['text'] . "\n";
6022 - }
6023 - }
6024 - }
6025 -
6026 - if (!empty($text_content) && $matches_used < $max_results) {
6027 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
6028 - $content .= trim($text_content) . "\n\n";
6029 - $content .= "Source: " . $filename . "\n\n";
6030 -
6031 - preg_match_all(
6032 - '#\bhttps?://[^\s<>"\']+#i',
6033 - $text_content,
6034 - $content_urls
6035 - );
6036 - if (!empty($content_urls[0])) {
6037 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6038 - }
6039 -
6040 - $matches_used++;
6041 -
6042 - $all_matches[] = [
6043 - 'document_id' => $filename,
6044 - 'similarity' => $score,
6045 - 'similarity_percentage' => round($score * 100, 2),
6046 - 'above_threshold' => true,
6047 - 'source_display' => $filename,
6048 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6049 - 'used_for_context' => true,
6050 - 'role_restriction' => 'public',
6051 - 'has_access' => true,
6052 - 'filtered_out' => false
6053 - ];
6054 - }
6055 - }
6056 - }
6057 - }
6058 - }
6059 - }
6060 - }
6061 - }
6062 - }
6063 -
6064 - // Store for testing panel
6065 - $this->last_similarity_analysis['top_matches'] = $all_matches;
6066 - $this->last_similarity_analysis['total_checked'] = count($all_matches);
6067 -
6068 - // Store unique valid URLs for validation
6069 - $this->current_valid_urls = array_unique($valid_urls);
6070 -
6071 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
6072 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
6073 -
6074 - //error_log("MXCHAT VECTORSTORE: ========== SEARCH COMPLETE ==========");
6075 - //error_log("MXCHAT VECTORSTORE: Matches used: " . $matches_used);
6076 - //error_log("MXCHAT VECTORSTORE: All matches count: " . count($all_matches));
6077 - //error_log("MXCHAT VECTORSTORE: Content length: " . strlen($content));
6078 - if ($matches_used > 0) {
6079 - //error_log("MXCHAT VECTORSTORE: Content preview: " . substr($content, 0, 500));
6080 - }
6081 -
6082 - // Check if citation links are enabled
6083 - $citation_links_enabled = ($mxchat_options['citation_links_toggle'] ?? 'on') === 'on';
6084 -
6085 - // Add response guidelines
6086 - if ($matches_used === 0) {
6087 - //error_log("MXCHAT VECTORSTORE: No matches found - returning empty reference message");
6088 - $content = "No reference information was found for this query.\n\n";
6089 - } else {
6090 - // Build response guidelines based on citation links setting
6091 - $content .= "\n## Response Guidelines ##\n" .
6092 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
6093 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
6094 - "If you don't have specific information or are uncertain about any details, it's always " .
6095 - "better to honestly say you don't know rather than making up or guessing at answers. " .
6096 - "When information is incomplete, let them know you are unsure.\n\n";
6097 -
6098 - // Only add hyperlink instructions if citation links are enabled
6099 - if ($citation_links_enabled) {
6100 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
6101 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
6102 - } else {
6103 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
6104 - "Simply provide helpful answers based on the reference information without citing sources.";
6105 - }
6106 - }
6107 -
6108 - //error_log("MXCHAT DEBUG: Vector Store search complete. Matches used: " . $matches_used);
6109 -
6110 - return trim($content);
6111 -}
6112 -
6113 -/**
6114 - * Check if the given model is an OpenAI chat model
6115 - *
6116 - * @param string $model The model ID
6117 - * @return bool True if it's an OpenAI model
6118 - */
6119 -private function is_openai_chat_model($model) {
6120 - $openai_prefixes = array('gpt-', 'o1-', 'o3-');
6121 - foreach ($openai_prefixes as $prefix) {
6122 - if (strpos($model, $prefix) === 0) {
6123 - return true;
6124 - }
6125 - }
6126 - return false;
6127 -}
6128 -
6129 -/**
6130 - * Get bot-specific Vector Store configuration
6131 - *
6132 - * @param string $bot_id The bot ID
6133 - * @return array Configuration array
6134 - */
6135 -private function get_bot_vectorstore_config($bot_id = 'default') {
6136 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
6137 -
6138 - // Default global settings
6139 - $default_config = array(
6140 - 'use_vectorstore' => ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1',
6141 - 'vectorstore_ids' => $vectorstore_options['mxchat_vectorstore_ids'] ?? '',
6142 - 'max_results' => $vectorstore_options['mxchat_vectorstore_max_results'] ?? 5
6143 - );
6144 -
6145 - // Allow multi-bot plugin to override with bot-specific settings
6146 - $bot_config = apply_filters('mxchat_get_bot_vectorstore_config', $default_config, $bot_id);
6147 -
6148 - // Preserve max_results from global settings if not set in bot config
6149 - if (!isset($bot_config['max_results'])) {
6150 - $bot_config['max_results'] = $default_config['max_results'];
6151 - }
6152 -
6153 - return $bot_config;
6154 -}
6155 -
6156 4044 private function mxchat_find_relevant_products($user_embedding) {
6157 4045 //error_log('MXChat Vector Search: Starting product search...');
6158 4046
6159 4047 // Retrieve the add-on settings from the database
@@ -6174,75 +4062,73 @@
6174 4062 }
6175 4063 private function find_relevant_products_wordpress($user_embedding) {
6176 4064 global $wpdb;
6177 4065 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
4066 + $cache_key = 'mxchat_system_prompt_embeddings';
4067 + $batch_size = 500;
6178 4068
6179 - if (!is_array($user_embedding)) {
6180 - return '';
6181 - }
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;
6182 4075
6183 - // Streaming top-K pass: scan rows in small batches, keep only the top 3
6184 - // results above the similarity threshold. Peak memory is bounded by
6185 - // $batch_size embedding rows plus a 3-element top list.
6186 - $batch_size = 250;
6187 - $similarity_threshold = 0.85;
6188 - $top_k = 3;
6189 - $top_results = [];
6190 - $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 + );
6191 4084
6192 - do {
6193 - $batch = $wpdb->get_results($wpdb->prepare(
6194 - "SELECT id, embedding_vector
6195 - FROM {$system_prompt_table}
6196 - LIMIT %d OFFSET %d",
6197 - $batch_size,
6198 - $offset
6199 - ));
4085 + $batch = $wpdb->get_results($query);
4086 + if (empty($batch)) {
4087 + break;
4088 + }
6200 4089
6201 - if (empty($batch)) {
6202 - break;
6203 - }
4090 + $embeddings = array_merge($embeddings, $batch);
4091 + $offset += $batch_size;
6204 4092
6205 - foreach ($batch as $row) {
6206 - $database_embedding = $row->embedding_vector
6207 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
6208 - : null;
4093 + unset($batch);
6209 4094
6210 - if (!is_array($database_embedding)) {
6211 - unset($database_embedding);
6212 - continue;
6213 - }
4095 + } while (true);
6214 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)) {
6215 4109 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
6216 - unset($database_embedding);
6217 -
6218 - if ($similarity < $similarity_threshold) {
6219 - continue;
6220 - }
6221 -
6222 - // Insert into bounded top-K (kept sorted descending)
6223 - if (count($top_results) < $top_k) {
6224 - $top_results[] = ['id' => $row->id, 'similarity' => $similarity];
6225 - usort($top_results, function ($a, $b) {
6226 - return $b['similarity'] <=> $a['similarity'];
6227 - });
6228 - } elseif ($similarity > $top_results[$top_k - 1]['similarity']) {
6229 - $top_results[$top_k - 1] = ['id' => $row->id, 'similarity' => $similarity];
6230 - usort($top_results, function ($a, $b) {
6231 - return $b['similarity'] <=> $a['similarity'];
6232 - });
6233 - }
4110 + $relevant_results[] = [
4111 + 'id' => $embedding->id,
4112 + 'similarity' => $similarity
4113 + ];
6234 4114 }
4115 + unset($database_embedding);
4116 + }
6235 4117
6236 - unset($batch);
6237 - $offset += $batch_size;
6238 - } while (true);
4118 + // Use fixed threshold for products
4119 + $similarity_threshold = 0.85;
6239 4120
6240 - if (empty($top_results)) {
6241 - return '';
6242 - }
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 + });
6243 4127
4128 + $top_results = array_slice($relevant_results, 0, 5);
6244 4129 $content = '';
4130 +
6245 4131 foreach ($top_results as $result) {
6246 4132 $chunk_content = $this->fetch_content_with_product_links($result['id']);
6247 4133 $content .= $chunk_content . "\n\n";
6248 4134 }
@@ -6351,61 +4237,23 @@
6351 4237
6352 4238 /**
6353 4239 * Get system instructions for a specific bot or default
6354 4240 * Checks for multi-bot add-on and uses bot-specific instructions if available
6355 - * Automatically strips URLs if citation links are disabled
6356 - * Replaces {visitor_name} placeholder with actual visitor name if available
6357 - *
6358 - * @param string $bot_id The bot ID to get instructions for
6359 - * @param string $session_id Optional session ID to lookup visitor name
6360 4241 */
6361 -private function get_system_instructions($bot_id = 'default', $session_id = '') {
6362 - $instructions = '';
6363 -
4242 +private function get_system_instructions($bot_id = 'default') {
6364 4243 // Check if multi-bot add-on is active
6365 4244 if (class_exists('MxChat_Multi_Bot_Core_Manager') && $bot_id !== 'default') {
6366 4245 // Get bot-specific options from multi-bot add-on
6367 4246 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
6368 -
4247 +
6369 4248 // If bot has custom system instructions, use those
6370 4249 if (!empty($bot_options['system_prompt_instructions'])) {
6371 - $instructions = $bot_options['system_prompt_instructions'];
4250 + return $bot_options['system_prompt_instructions'];
6372 4251 }
6373 4252 }
6374 -
4253 +
6375 4254 // Fall back to default system instructions
6376 - if (empty($instructions)) {
6377 - $instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6378 - }
6379 -
6380 - // Check if citation links are disabled - if so, strip URLs from instructions
6381 - $fresh_options = get_option('mxchat_options', []);
6382 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
6383 -
6384 - if (!$citation_links_enabled && !empty($instructions)) {
6385 - $instructions = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $instructions);
6386 - $instructions = preg_replace('/\s+/', ' ', trim($instructions)); // Clean up extra spaces
6387 - }
6388 -
6389 - // Replace {visitor_name} placeholder with actual visitor name if available
6390 - if (!empty($instructions) && !empty($session_id) && stripos($instructions, '{visitor_name}') !== false) {
6391 - $name_option_key = "mxchat_name_{$session_id}";
6392 - $visitor_name = get_option($name_option_key, '');
6393 -
6394 - if (!empty($visitor_name)) {
6395 - $instructions = str_ireplace('{visitor_name}', sanitize_text_field($visitor_name), $instructions);
6396 - } else {
6397 - // Remove placeholder if no name is available
6398 - $instructions = str_ireplace('{visitor_name}', '', $instructions);
6399 - $instructions = preg_replace('/\s{2,}/', ' ', trim($instructions)); // Clean up extra spaces
6400 - }
6401 - }
6402 -
6403 - // Allow developers to filter system instructions and process shortcodes
6404 - $instructions = apply_filters('mxchat_system_instructions', $instructions, $bot_id, $session_id);
6405 - $instructions = do_shortcode($instructions);
6406 -
6407 - return $instructions;
4255 + return isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6408 4256 }
6409 4257 /**
6410 4258 * Get the current bot ID from session or request context
6411 4259 */
@@ -6425,9 +4273,9 @@
6425 4273
6426 4274 // Fall back to default
6427 4275 return 'default';
6428 4276 }
6429 -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') {
6430 4278 try {
6431 4279 if (!$relevant_content) {
6432 4280 $error_response = [
6433 4281 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
@@ -6433,73 +4281,22 @@
6433 4281 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
6434 4282 'error_code' => 'no_relevant_content'
6435 4283 ];
6436 4284
4285 + // Add testing data to error response if available
6437 4286 if ($testing_data !== null) {
6438 4287 $error_response['testing_data'] = $testing_data;
4288 + //error_log("MxChat Testing: Added testing data to no_relevant_content error");
6439 4289 }
6440 4290
6441 4291 return $error_response;
6442 4292 }
6443 4293
4294 + // Ensure conversation_history is an array
6444 4295 if (!is_array($conversation_history)) {
6445 4296 $conversation_history = array();
6446 4297 }
6447 4298
6448 - // Check if this is an OpenRouter model
6449 - if ($selected_model === 'openrouter') {
6450 - // Get the actual OpenRouter model from options
6451 - $openrouter_selected_model = $this->options['openrouter_selected_model'] ?? '';
6452 -
6453 - if (empty($openrouter_selected_model)) {
6454 - $error_response = [
6455 - 'error' => esc_html__('No OpenRouter model selected. Please select a model in settings.', 'mxchat'),
6456 - 'error_code' => 'no_openrouter_model_selected'
6457 - ];
6458 - if ($testing_data !== null) {
6459 - $error_response['testing_data'] = $testing_data;
6460 - }
6461 - return $error_response;
6462 - }
6463 -
6464 - if (empty($openrouter_api_key)) {
6465 - $error_response = [
6466 - 'error' => esc_html__('OpenRouter API key is not configured', 'mxchat'),
6467 - 'error_code' => 'missing_openrouter_api_key'
6468 - ];
6469 - if ($testing_data !== null) {
6470 - $error_response['testing_data'] = $testing_data;
6471 - }
6472 - return $error_response;
6473 - }
6474 -
6475 - if ($streaming) {
6476 - return $this->mxchat_generate_response_openrouter_stream(
6477 - $openrouter_selected_model,
6478 - $openrouter_api_key,
6479 - $conversation_history,
6480 - $relevant_content,
6481 - $session_id,
6482 - $testing_data
6483 - );
6484 - } else {
6485 - $response = $this->mxchat_generate_response_openrouter(
6486 - $openrouter_selected_model,
6487 - $openrouter_api_key,
6488 - $conversation_history,
6489 - $relevant_content
6490 - );
6491 - }
6492 -
6493 - if (is_array($response) && isset($response['error'])) {
6494 - if ($testing_data !== null) {
6495 - $response['testing_data'] = $testing_data;
6496 - }
6497 - return $response;
6498 - }
6499 -
6500 - return $response;
6501 - }
6502 4299
6503 4300 // Extract model prefix to determine the provider
6504 4301 $model_parts = explode('-', $selected_model);
6505 4302 $provider = strtolower($model_parts[0]);
@@ -6542,9 +4339,9 @@
6542 4339 $claude_api_key,
6543 4340 $conversation_history,
6544 4341 $relevant_content,
6545 4342 $session_id,
6546 - $testing_data
4343 + $testing_data // Pass testing data
6547 4344 );
6548 4345 } else {
6549 4346 $response = $this->mxchat_generate_response_claude(
6550 4347 $selected_model,
@@ -6572,9 +4369,9 @@
6572 4369 $xai_api_key,
6573 4370 $conversation_history,
6574 4371 $relevant_content,
6575 4372 $session_id,
6576 - $testing_data
4373 + $testing_data // Pass testing data
6577 4374 );
6578 4375 } else {
6579 4376 $response = $this->mxchat_generate_response_xai(
6580 4377 $selected_model,
@@ -6602,9 +4399,9 @@
6602 4399 $deepseek_api_key,
6603 4400 $conversation_history,
6604 4401 $relevant_content,
6605 4402 $session_id,
6606 - $testing_data
4403 + $testing_data // Pass testing data
6607 4404 );
6608 4405 } else {
6609 4406 $response = $this->mxchat_generate_response_deepseek(
6610 4407 $selected_model,
@@ -6626,27 +4423,9 @@
6626 4423 $error_response['testing_data'] = $testing_data;
6627 4424 }
6628 4425 return $error_response;
6629 4426 }
6630 -
6631 - // Check if web search is enabled for this OpenAI model
6632 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6633 - // Models that don't support web search
6634 - $unsupported_web_search_models = array('gpt-4.1-nano');
6635 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6636 -
6637 - if ($web_search_enabled && $model_supports_web_search) {
6638 - // Use Responses API (required for some models, or when web search is enabled)
6639 - return $this->mxchat_generate_response_openai_web_search(
6640 - $selected_model,
6641 - $api_key,
6642 - $conversation_history,
6643 - $relevant_content,
6644 - $session_id,
6645 - $testing_data,
6646 - $streaming
6647 - );
6648 - } elseif ($streaming) {
4427 + if ($streaming) {
6649 4428 return $this->mxchat_generate_response_openai_stream(
6650 4429 $selected_model,
6651 4430 $api_key,
6652 4431 $conversation_history,
@@ -6651,9 +4430,9 @@
6651 4430 $api_key,
6652 4431 $conversation_history,
6653 4432 $relevant_content,
6654 4433 $session_id,
6655 - $testing_data
4434 + $testing_data // Pass testing data
6656 4435 );
6657 4436 } else {
6658 4437 $response = $this->mxchat_generate_response_openai(
6659 4438 $selected_model,
@@ -6664,8 +4443,9 @@
6664 4443 }
6665 4444 break;
6666 4445
6667 4446 default:
4447 + // Default to OpenAI for custom models or unrecognized prefixes
6668 4448 if (empty($api_key)) {
6669 4449 $error_response = [
6670 4450 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
6671 4451 'error_code' => 'missing_openai_api_key'
@@ -6674,25 +4454,9 @@
6674 4454 $error_response['testing_data'] = $testing_data;
6675 4455 }
6676 4456 return $error_response;
6677 4457 }
6678 -
6679 - // Check if web search is enabled (default case also handles OpenAI models)
6680 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6681 - $unsupported_web_search_models = array('gpt-4.1-nano');
6682 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6683 -
6684 - if ($web_search_enabled && $model_supports_web_search) {
6685 - return $this->mxchat_generate_response_openai_web_search(
6686 - $selected_model,
6687 - $api_key,
6688 - $conversation_history,
6689 - $relevant_content,
6690 - $session_id,
6691 - $testing_data,
6692 - $streaming
6693 - );
6694 - } elseif ($streaming) {
4458 + if ($streaming) {
6695 4459 return $this->mxchat_generate_response_openai_stream(
6696 4460 $selected_model,
6697 4461 $api_key,
6698 4462 $conversation_history,
@@ -6697,9 +4461,9 @@
6697 4461 $api_key,
6698 4462 $conversation_history,
6699 4463 $relevant_content,
6700 4464 $session_id,
6701 - $testing_data
4465 + $testing_data // Pass testing data
6702 4466 );
6703 4467 } else {
6704 4468 $response = $this->mxchat_generate_response_openai(
6705 4469 $selected_model,
@@ -6710,18 +4474,24 @@
6710 4474 }
6711 4475 break;
6712 4476 }
6713 4477
4478 + // Check if the response is an error array from the provider-specific function
6714 4479 if (is_array($response) && isset($response['error'])) {
4480 + // Add testing data to error response if available
6715 4481 if ($testing_data !== null) {
6716 4482 $response['testing_data'] = $testing_data;
4483 + //error_log("MxChat Testing: Added testing data to provider error response");
6717 4484 }
6718 - return $response;
4485 + return $response; // Pass through the error with testing data
6719 4486 }
6720 4487
4488 + // For successful non-streaming responses, we don't add testing data here
4489 + // because it will be added in the main handler
6721 4490 return $response;
6722 4491
6723 4492 } catch (Exception $e) {
4493 + //error_log('MXChat Error: ' . $e->getMessage());
6724 4494 $error_response = [
6725 4495 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
6726 4496 'error_code' => 'system_exception',
6727 4497 'exception_details' => $e->getMessage()
@@ -6726,229 +4496,24 @@
6726 4496 'error_code' => 'system_exception',
6727 4497 'exception_details' => $e->getMessage()
6728 4498 ];
6729 4499
4500 + // Add testing data to exception response if available
6730 4501 if ($testing_data !== null) {
6731 4502 $error_response['testing_data'] = $testing_data;
4503 + //error_log("MxChat Testing: Added testing data to exception response");
6732 4504 }
6733 4505
6734 4506 return $error_response;
6735 4507 }
6736 4508 }
6737 -private function mxchat_generate_response_openrouter_stream($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
6738 - try {
6739 - $bot_id = $this->get_current_bot_id($session_id);
6740 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6741 -
6742 - if (!is_array($conversation_history)) {
6743 - $conversation_history = array();
6744 - }
6745 4509
6746 - $formatted_conversation = array();
6747 -
6748 - $formatted_conversation[] = array(
6749 - 'role' => 'system',
6750 - 'content' => $system_prompt_instructions . " " . $relevant_content
6751 - );
6752 -
6753 - foreach ($conversation_history as $message) {
6754 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6755 - $role = $message['role'];
6756 - if ($role === 'bot' || $role === 'agent') {
6757 - $role = 'assistant';
6758 - }
6759 - if (!in_array($role, ['system', 'assistant', 'user'])) {
6760 - $role = 'user';
6761 - }
6762 - $formatted_conversation[] = array(
6763 - 'role' => $role,
6764 - 'content' => $message['content']
6765 - );
6766 - }
6767 - }
6768 -
6769 - if (headers_sent() || !function_exists('curl_init')) {
6770 - $regular_response = $this->mxchat_generate_response_openrouter(
6771 - $selected_model,
6772 - $openrouter_api_key,
6773 - $conversation_history,
6774 - $relevant_content
6775 - );
6776 -
6777 - // Save bot response to transcript
6778 - if (!empty($regular_response) && !empty($session_id)) {
6779 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
6780 - }
6781 -
6782 - $response_data = [
6783 - 'text' => $regular_response,
6784 - 'html' => '',
6785 - 'session_id' => $session_id
6786 - ];
6787 -
6788 - if ($testing_data !== null) {
6789 - $response_data['testing_data'] = $testing_data;
6790 - }
6791 -
6792 - header('Content-Type: application/json');
6793 - echo json_encode($response_data);
6794 - return true;
6795 - }
6796 -
6797 - $body = json_encode([
6798 - 'model' => $selected_model,
6799 - 'messages' => $formatted_conversation,
6800 - 'temperature' => 1,
6801 - 'stream' => true
6802 - ]);
6803 -
6804 - // Setup streaming headers now that we know we're actually streaming
6805 - $this->setup_streaming_headers();
6806 -
6807 - $ch = curl_init();
6808 - curl_setopt($ch, CURLOPT_URL, 'https://openrouter.ai/api/v1/chat/completions');
6809 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
6810 - curl_setopt($ch, CURLOPT_POST, true);
6811 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
6812 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
6813 - 'Content-Type: application/json',
6814 - 'Authorization: Bearer ' . $openrouter_api_key,
6815 - 'HTTP-Referer: ' . home_url(),
6816 - 'X-Title: ' . get_bloginfo('name')
6817 - ));
6818 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
6819 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
6820 -
6821 - $full_response = '';
6822 - $stream_started = false;
6823 - $buffer = '';
6824 -
6825 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
6826 - if (!$stream_started && $testing_data !== null) {
6827 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
6828 - flush();
6829 - $stream_started = true;
6830 - }
6831 -
6832 - $buffer .= $data;
6833 - $lines = explode("\n", $buffer);
6834 - $buffer = array_pop($lines);
6835 -
6836 - foreach ($lines as $line) {
6837 - if (trim($line) === '') {
6838 - continue;
6839 - }
6840 -
6841 - if (strpos($line, 'data: ') !== 0) {
6842 - continue;
6843 - }
6844 -
6845 - $json_str = substr($line, 6);
6846 -
6847 - if (trim($json_str) === '[DONE]') {
6848 - echo "data: [DONE]\n\n";
6849 - flush();
6850 - continue;
6851 - }
6852 -
6853 - $json = json_decode(trim($json_str), true);
6854 - if ($json && isset($json['choices'][0]['delta']['content'])) {
6855 - $content = $json['choices'][0]['delta']['content'];
6856 - $full_response .= $content;
6857 -
6858 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
6859 - flush();
6860 - }
6861 - }
6862 -
6863 - return strlen($data);
6864 - });
6865 -
6866 - $response = curl_exec($ch);
6867 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
6868 -
6869 - if (curl_errno($ch) || $http_code !== 200) {
6870 - curl_close($ch);
6871 -
6872 - $regular_response = $this->mxchat_generate_response_openrouter(
6873 - $selected_model,
6874 - $openrouter_api_key,
6875 - $conversation_history,
6876 - $relevant_content
6877 - );
6878 -
6879 - $response_data = [
6880 - 'text' => $regular_response,
6881 - 'html' => '',
6882 - 'session_id' => $session_id
6883 - ];
6884 -
6885 - if ($testing_data !== null) {
6886 - $response_data['testing_data'] = $testing_data;
6887 - }
6888 -
6889 - header('Content-Type: application/json');
6890 - echo json_encode($response_data);
6891 - return true;
6892 - }
6893 -
6894 - curl_close($ch);
6895 -
6896 - if (!empty($full_response) && !empty($session_id)) {
6897 - // Prepare RAG context for streaming response
6898 - $rag_context_for_storage = null;
6899 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
6900 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
6901 -
6902 - if ($has_rag_data || $has_action_data) {
6903 - $rag_context_for_storage = [];
6904 -
6905 - if ($has_rag_data) {
6906 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
6907 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
6908 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
6909 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
6910 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
6911 - }
6912 -
6913 - if ($has_action_data) {
6914 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
6915 - }
6916 - }
6917 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
6918 - }
6919 -
6920 - return true;
6921 -
6922 - } catch (Exception $e) {
6923 - $regular_response = $this->mxchat_generate_response_openrouter(
6924 - $selected_model,
6925 - $openrouter_api_key,
6926 - $conversation_history,
6927 - $relevant_content
6928 - );
6929 -
6930 - $response_data = [
6931 - 'text' => $regular_response,
6932 - 'html' => '',
6933 - 'session_id' => $session_id
6934 - ];
6935 -
6936 - if ($testing_data !== null) {
6937 - $response_data['testing_data'] = $testing_data;
6938 - }
6939 -
6940 - header('Content-Type: application/json');
6941 - echo json_encode($response_data);
6942 - return true;
6943 - }
6944 -}
6945 4510 private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
6946 4511 try {
6947 4512 $bot_id = $this->get_current_bot_id($session_id);
6948 4513
6949 4514 // Get system prompt instructions using centralized function
6950 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
4515 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
6951 4516
6952 4517 // Ensure conversation_history is an array
6953 4518 if (!is_array($conversation_history)) {
6954 4519 $conversation_history = array();
@@ -6987,13 +4552,8 @@
6987 4552 $conversation_history,
6988 4553 $relevant_content
6989 4554 );
6990 4555
6991 - // Save bot response to transcript
6992 - if (!empty($regular_response) && !empty($session_id)) {
6993 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
6994 - }
6995 -
6996 4556 $response_data = [
6997 4557 'text' => $regular_response,
6998 4558 'html' => '',
6999 4559 'session_id' => $session_id
@@ -7007,45 +4567,16 @@
7007 4567 echo json_encode($response_data);
7008 4568 return true;
7009 4569 }
7010 4570
7011 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
7012 - $is_gpt5_model = (
7013 - strpos($selected_model, 'gpt-5') === 0 ||
7014 - $selected_model === 'gpt-5.2' ||
7015 - $selected_model === 'gpt-5.1-2025-11-13' ||
7016 - $selected_model === 'gpt-5' ||
7017 - $selected_model === 'gpt-5-mini' ||
7018 - $selected_model === 'gpt-5-nano'
7019 - );
7020 -
7021 - // Build request body with optimal settings for fast streaming
7022 - $request_body = [
4571 + // Prepare the request body with stream: true
4572 + $body = json_encode([
7023 4573 'model' => $selected_model,
7024 4574 'messages' => $formatted_conversation,
7025 4575 'temperature' => 1,
7026 4576 'stream' => true
7027 - ];
4577 + ]);
7028 4578
7029 - // Add reasoning_effort only for GPT-5 models that support it
7030 - // These chat models don't support reasoning_effort parameter
7031 - $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');
7032 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
7033 - // GPT-5.1 uses 'low' instead of 'minimal'
7034 - if ($selected_model === 'gpt-5.1-2025-11-13') {
7035 - $request_body['reasoning_effort'] = 'low';
7036 - } elseif ($selected_model === 'gpt-5.4') {
7037 - $request_body['reasoning_effort'] = 'none';
7038 - } else {
7039 - $request_body['reasoning_effort'] = 'minimal';
7040 - }
7041 - }
7042 -
7043 - $body = json_encode($request_body);
7044 -
7045 - // Setup streaming headers now that we know we're actually streaming
7046 - $this->setup_streaming_headers();
7047 -
7048 4579 // Use cURL for streaming support
7049 4580 $ch = curl_init();
7050 4581 curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
7051 4582 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -7118,11 +4649,10 @@
7118 4649 $response = curl_exec($ch);
7119 4650 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7120 4651
7121 4652 if (curl_errno($ch) || $http_code !== 200) {
7122 - $curl_error = curl_error($ch);
7123 4653 curl_close($ch);
7124 -
4654 +
7125 4655 // Fallback to regular response
7126 4656 $regular_response = $this->mxchat_generate_response_openai(
7127 4657 $selected_model,
7128 4658 $api_key,
@@ -7128,34 +4658,19 @@
7128 4658 $api_key,
7129 4659 $conversation_history,
7130 4660 $relevant_content
7131 4661 );
7132 -
7133 - // FIXED: Check if regular response returned an error
7134 - if (is_array($regular_response) && isset($regular_response['error'])) {
7135 - // Send error in SSE format since we're in streaming mode
7136 - echo "data: " . json_encode([
7137 - 'error' => true,
7138 - 'error_message' => $regular_response['error'],
7139 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7140 - 'text' => $regular_response['error'],
7141 - 'message' => $regular_response['error']
7142 - ]) . "\n\n";
7143 - echo "data: [DONE]\n\n";
7144 - flush();
7145 - return true;
7146 - }
7147 -
4662 +
7148 4663 $response_data = [
7149 4664 'text' => $regular_response,
7150 4665 'html' => '',
7151 4666 'session_id' => $session_id
7152 4667 ];
7153 -
4668 +
7154 4669 if ($testing_data !== null) {
7155 4670 $response_data['testing_data'] = $testing_data;
7156 4671 }
7157 -
4672 +
7158 4673 header('Content-Type: application/json');
7159 4674 echo json_encode($response_data);
7160 4675 return true;
7161 4676 }
@@ -7160,36 +4675,16 @@
7160 4675 return true;
7161 4676 }
7162 4677
7163 4678 curl_close($ch);
7164 -
4679 +
7165 4680 // Save the complete response to maintain chat persistence
7166 4681 if (!empty($full_response) && !empty($session_id)) {
7167 - // Prepare RAG context for streaming response
7168 - $rag_context_for_storage = null;
7169 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7170 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7171 -
7172 - if ($has_rag_data || $has_action_data) {
7173 - $rag_context_for_storage = [];
7174 -
7175 - if ($has_rag_data) {
7176 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7177 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7178 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7179 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7180 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7181 - }
7182 -
7183 - if ($has_action_data) {
7184 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7185 - }
7186 - }
7187 - $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);
7188 4683 }
7189 -
4684 +
7190 4685 return true; // Indicate streaming completed successfully
7191 -
4686 +
7192 4687 } catch (Exception $e) {
7193 4688 // Fallback to regular response
7194 4689 $regular_response = $this->mxchat_generate_response_openai(
7195 4690 $selected_model,
@@ -7196,394 +4691,24 @@
7196 4691 $api_key,
7197 4692 $conversation_history,
7198 4693 $relevant_content
7199 4694 );
7200 -
7201 - // FIXED: Check if regular response returned an error
7202 - if (is_array($regular_response) && isset($regular_response['error'])) {
7203 - // Send error in SSE format since we're in streaming mode
7204 - echo "data: " . json_encode([
7205 - 'error' => true,
7206 - 'error_message' => $regular_response['error'],
7207 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7208 - 'text' => $regular_response['error'],
7209 - 'message' => $regular_response['error']
7210 - ]) . "\n\n";
7211 - echo "data: [DONE]\n\n";
7212 - flush();
7213 - return true;
7214 - }
7215 -
4695 +
7216 4696 $response_data = [
7217 4697 'text' => $regular_response,
7218 4698 'html' => '',
7219 4699 'session_id' => $session_id
7220 4700 ];
7221 -
4701 +
7222 4702 if ($testing_data !== null) {
7223 4703 $response_data['testing_data'] = $testing_data;
7224 4704 }
7225 -
4705 +
7226 4706 header('Content-Type: application/json');
7227 4707 echo json_encode($response_data);
7228 4708 return true;
7229 4709 }
7230 4710 }
7231 -
7232 -/**
7233 - * Generate response using OpenAI Responses API with web search tool
7234 - * This uses the newer Responses API which supports web search functionality
7235 - */
7236 -private function mxchat_generate_response_openai_web_search($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null, $streaming = false) {
7237 - try {
7238 - $bot_id = $this->get_current_bot_id($session_id);
7239 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7240 -
7241 - if (!is_array($conversation_history)) {
7242 - $conversation_history = array();
7243 - }
7244 -
7245 - // Build the input for Responses API
7246 - // The Responses API uses a different format - we need to construct the input properly
7247 - $input_parts = [];
7248 -
7249 - // Add system instructions as context
7250 - $system_context = $system_prompt_instructions . "\n\n" . $relevant_content;
7251 -
7252 - // Build conversation as input items for Responses API
7253 - foreach ($conversation_history as $message) {
7254 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7255 - $role = $message['role'];
7256 - if ($role === 'bot' || $role === 'agent') {
7257 - $role = 'assistant';
7258 - }
7259 - if (!in_array($role, ['assistant', 'user'])) {
7260 - $role = 'user';
7261 - }
7262 - $input_parts[] = [
7263 - 'type' => 'message',
7264 - 'role' => $role,
7265 - 'content' => $message['content']
7266 - ];
7267 - }
7268 - }
7269 -
7270 - // Build request body for Responses API
7271 - $request_body = [
7272 - 'model' => $selected_model,
7273 - 'input' => $input_parts,
7274 - 'instructions' => $system_context,
7275 - 'stream' => $streaming
7276 - ];
7277 -
7278 - // Only add web search tool if web search is enabled in settings
7279 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
7280 - if ($web_search_enabled) {
7281 - $request_body['tools'] = [
7282 - ['type' => 'web_search']
7283 - ];
7284 - }
7285 -
7286 - // Add reasoning effort for supported models
7287 - $is_gpt5_model = strpos($selected_model, 'gpt-5') === 0;
7288 - $no_reasoning_web = array('gpt-5.2', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
7289 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_web, true)) {
7290 - if ($selected_model === 'gpt-5.1-2025-11-13') {
7291 - $request_body['reasoning'] = ['effort' => 'low'];
7292 - } elseif ($selected_model === 'gpt-5.4') {
7293 - $request_body['reasoning'] = ['effort' => 'low'];
7294 - }
7295 - }
7296 -
7297 - //error_log("MXCHAT WEB SEARCH: Request body: " . json_encode($request_body));
7298 -
7299 - if ($streaming) {
7300 - return $this->mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data);
7301 - } else {
7302 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7303 - }
7304 -
7305 - } catch (Exception $e) {
7306 - //error_log("MXCHAT WEB SEARCH ERROR: " . $e->getMessage());
7307 - return [
7308 - 'error' => sprintf(esc_html__('Web search error: %s', 'mxchat'), esc_html($e->getMessage())),
7309 - 'error_code' => 'web_search_exception'
7310 - ];
7311 - }
7312 -}
7313 -
7314 -/**
7315 - * Handle non-streaming web search response
7316 - */
7317 -private function mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7318 - $request_body['stream'] = false;
7319 -
7320 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
7321 - 'headers' => array(
7322 - 'Authorization' => 'Bearer ' . $api_key,
7323 - 'Content-Type' => 'application/json'
7324 - ),
7325 - 'body' => json_encode($request_body),
7326 - 'timeout' => 90
7327 - ));
7328 -
7329 - if (is_wp_error($response)) {
7330 - //error_log("MXCHAT WEB SEARCH ERROR: WP Error: " . $response->get_error_message());
7331 - return [
7332 - 'error' => esc_html__('Failed to connect to OpenAI web search API', 'mxchat'),
7333 - 'error_code' => 'web_search_connection_error'
7334 - ];
7335 - }
7336 -
7337 - $response_code = wp_remote_retrieve_response_code($response);
7338 - $response_body = wp_remote_retrieve_body($response);
7339 -
7340 - //error_log("MXCHAT WEB SEARCH: Response code: " . $response_code);
7341 - //error_log("MXCHAT WEB SEARCH: Response body (first 2000): " . substr($response_body, 0, 2000));
7342 -
7343 - if ($response_code !== 200) {
7344 - $error_data = json_decode($response_body, true);
7345 - $error_message = $error_data['error']['message'] ?? 'Unknown API error';
7346 - return [
7347 - 'error' => sprintf(esc_html__('OpenAI API error: %s', 'mxchat'), esc_html($error_message)),
7348 - 'error_code' => 'web_search_api_error'
7349 - ];
7350 - }
7351 -
7352 - $result = json_decode($response_body, true);
7353 -
7354 - if (json_last_error() !== JSON_ERROR_NONE) {
7355 - return [
7356 - 'error' => esc_html__('Invalid response from OpenAI', 'mxchat'),
7357 - 'error_code' => 'web_search_json_error'
7358 - ];
7359 - }
7360 -
7361 - // Extract the response text and citations from Responses API format
7362 - $output_text = '';
7363 - $citations = [];
7364 -
7365 - if (isset($result['output'])) {
7366 - foreach ($result['output'] as $output_item) {
7367 - if ($output_item['type'] === 'message' && isset($output_item['content'])) {
7368 - foreach ($output_item['content'] as $content_item) {
7369 - if ($content_item['type'] === 'output_text') {
7370 - $output_text .= $content_item['text'];
7371 -
7372 - // Extract citations/annotations
7373 - if (isset($content_item['annotations'])) {
7374 - foreach ($content_item['annotations'] as $annotation) {
7375 - if ($annotation['type'] === 'url_citation') {
7376 - $citations[] = [
7377 - 'url' => $annotation['url'],
7378 - 'title' => $annotation['title'] ?? ''
7379 - ];
7380 - }
7381 - }
7382 - }
7383 - }
7384 - }
7385 - }
7386 - }
7387 - }
7388 -
7389 - // If we have citations, append them to the response
7390 - if (!empty($citations)) {
7391 - $output_text .= "\n\n**Sources:**\n";
7392 - $seen_urls = [];
7393 - foreach ($citations as $citation) {
7394 - if (!in_array($citation['url'], $seen_urls)) {
7395 - $seen_urls[] = $citation['url'];
7396 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7397 - $output_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7398 - }
7399 - }
7400 - }
7401 -
7402 - // Transcript save is handled by the main handler (mxchat_handle_chat_request)
7403 - // which includes rag_context for the "sources" link in transcripts.
7404 -
7405 - return $output_text;
7406 -}
7407 -
7408 -/**
7409 - * Handle streaming web search response using Responses API
7410 - */
7411 -private function mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7412 - $request_body['stream'] = true;
7413 -
7414 - // Check if we can stream
7415 - if (headers_sent() || !function_exists('curl_init')) {
7416 - // Fallback to non-streaming
7417 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7418 - }
7419 -
7420 - // Setup streaming headers
7421 - $this->setup_streaming_headers();
7422 -
7423 - $ch = curl_init();
7424 - curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/responses');
7425 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7426 - curl_setopt($ch, CURLOPT_POST, true);
7427 - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request_body));
7428 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7429 - 'Content-Type: application/json',
7430 - 'Authorization: Bearer ' . $api_key
7431 - ));
7432 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7433 - curl_setopt($ch, CURLOPT_TIMEOUT, 120);
7434 -
7435 - $full_response = '';
7436 - $stream_started = false;
7437 - $buffer = '';
7438 - $citations = [];
7439 -
7440 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$citations, $testing_data) {
7441 - // Send testing data as first event if available
7442 - if (!$stream_started && $testing_data !== null) {
7443 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7444 - flush();
7445 - $stream_started = true;
7446 - }
7447 -
7448 - $buffer .= $data;
7449 - $lines = explode("\n", $buffer);
7450 - $buffer = array_pop($lines);
7451 -
7452 - foreach ($lines as $line) {
7453 - if (trim($line) === '') continue;
7454 - if (strpos($line, 'data: ') !== 0) continue;
7455 -
7456 - $json_str = substr($line, 6);
7457 -
7458 - if (trim($json_str) === '[DONE]') {
7459 - // Append citations if we have any
7460 - if (!empty($citations)) {
7461 - $citation_text = "\n\n**Sources:**\n";
7462 - $seen_urls = [];
7463 - foreach ($citations as $citation) {
7464 - if (!in_array($citation['url'], $seen_urls)) {
7465 - $seen_urls[] = $citation['url'];
7466 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7467 - $citation_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7468 - }
7469 - }
7470 - echo "data: " . json_encode(['content' => $citation_text]) . "\n\n";
7471 - $full_response .= $citation_text;
7472 - flush();
7473 - }
7474 - echo "data: [DONE]\n\n";
7475 - flush();
7476 - continue;
7477 - }
7478 -
7479 - $json = json_decode(trim($json_str), true);
7480 - if (!$json) continue;
7481 -
7482 - // Handle Responses API streaming events
7483 - // The format is different from Chat Completions
7484 - if (isset($json['type'])) {
7485 - switch ($json['type']) {
7486 - case 'response.output_text.delta':
7487 - // Text content delta
7488 - if (isset($json['delta'])) {
7489 - $content = $json['delta'];
7490 - $full_response .= $content;
7491 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
7492 - flush();
7493 - }
7494 - break;
7495 -
7496 - case 'response.output_item.done':
7497 - // Check for citations in completed items
7498 - if (isset($json['item']['content'])) {
7499 - foreach ($json['item']['content'] as $content_item) {
7500 - if (isset($content_item['annotations'])) {
7501 - foreach ($content_item['annotations'] as $annotation) {
7502 - if ($annotation['type'] === 'url_citation') {
7503 - $citations[] = [
7504 - 'url' => $annotation['url'],
7505 - 'title' => $annotation['title'] ?? ''
7506 - ];
7507 - }
7508 - }
7509 - }
7510 - }
7511 - }
7512 - break;
7513 - }
7514 - }
7515 - }
7516 -
7517 - return strlen($data);
7518 - });
7519 -
7520 - $response = curl_exec($ch);
7521 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7522 -
7523 - if (curl_errno($ch) || $http_code !== 200) {
7524 - $curl_error = curl_error($ch);
7525 - curl_close($ch);
7526 -
7527 - //error_log("MXCHAT WEB SEARCH STREAM ERROR: HTTP $http_code, cURL error: $curl_error");
7528 -
7529 - // Fallback to non-streaming
7530 - $fallback_response = $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7531 -
7532 - if (is_array($fallback_response) && isset($fallback_response['error'])) {
7533 - echo "data: " . json_encode([
7534 - 'error' => true,
7535 - 'error_message' => $fallback_response['error'],
7536 - 'error_code' => $fallback_response['error_code'] ?? 'web_search_error'
7537 - ]) . "\n\n";
7538 - echo "data: [DONE]\n\n";
7539 - flush();
7540 - return true;
7541 - }
7542 -
7543 - $response_data = [
7544 - 'text' => $fallback_response,
7545 - 'html' => '',
7546 - 'session_id' => $session_id
7547 - ];
7548 - if ($testing_data !== null) {
7549 - $response_data['testing_data'] = $testing_data;
7550 - }
7551 - header('Content-Type: application/json');
7552 - echo json_encode($response_data);
7553 - return true;
7554 - }
7555 -
7556 - curl_close($ch);
7557 -
7558 - // Save the complete response with RAG context so the "sources" link
7559 - // appears in transcripts — mirrors the pattern used by Claude/OpenAI streaming.
7560 - if (!empty($full_response) && !empty($session_id)) {
7561 - $rag_context_for_storage = null;
7562 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7563 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7564 -
7565 - if ($has_rag_data || $has_action_data) {
7566 - $rag_context_for_storage = [];
7567 -
7568 - if ($has_rag_data) {
7569 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7570 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7571 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7572 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7573 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7574 - }
7575 -
7576 - if ($has_action_data) {
7577 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7578 - }
7579 - }
7580 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
7581 - }
7582 -
7583 - return true;
7584 -}
7585 -
7586 4711 private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7587 4712 try {
7588 4713 // Get bot ID from session or request
7589 4714 $bot_id = $this->get_current_bot_id($session_id);
@@ -7588,9 +4713,9 @@
7588 4713 // Get bot ID from session or request
7589 4714 $bot_id = $this->get_current_bot_id($session_id);
7590 4715
7591 4716 // Get system prompt instructions using centralized function
7592 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
4717 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
7593 4718 // Ensure conversation_history is an array
7594 4719 if (!is_array($conversation_history)) {
7595 4720 $conversation_history = array();
7596 4721 }
@@ -7642,13 +4767,8 @@
7642 4767 array_slice($conversation_history, 0, -1), // Remove the added content
7643 4768 $relevant_content
7644 4769 );
7645 4770
7646 - // Save bot response to transcript
7647 - if (!empty($regular_response) && !empty($session_id)) {
7648 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7649 - }
7650 -
7651 4771 // Return as JSON with testing data
7652 4772 $response_data = [
7653 4773 'text' => $regular_response,
7654 4774 'html' => '',
@@ -7667,11 +4787,8 @@
7667 4787 echo json_encode($response_data);
7668 4788 return true; // Indicate we handled the response
7669 4789 }
7670 4790
7671 - // Setup streaming headers now that we know we're actually streaming
7672 - $this->setup_streaming_headers();
7673 -
7674 4791 // Use cURL for streaming support
7675 4792 $ch = curl_init();
7676 4793 curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
7677 4794 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -7776,35 +4893,20 @@
7776 4893 $claude_api_key,
7777 4894 array_slice($conversation_history, 0, -1), // Remove the added content
7778 4895 $relevant_content
7779 4896 );
7780 -
7781 - // FIXED: Check if regular response returned an error
7782 - if (is_array($regular_response) && isset($regular_response['error'])) {
7783 - // Send error in SSE format since we're in streaming mode
7784 - echo "data: " . json_encode([
7785 - 'error' => true,
7786 - 'error_message' => $regular_response['error'],
7787 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7788 - 'text' => $regular_response['error'],
7789 - 'message' => $regular_response['error']
7790 - ]) . "\n\n";
7791 - echo "data: [DONE]\n\n";
7792 - flush();
7793 - return true;
7794 - }
7795 -
4897 +
7796 4898 $response_data = [
7797 4899 'text' => $regular_response,
7798 4900 'html' => '',
7799 4901 'session_id' => $session_id
7800 4902 ];
7801 -
4903 +
7802 4904 if ($testing_data !== null) {
7803 4905 $response_data['testing_data'] = $testing_data;
7804 4906 //error_log("MxChat Testing: Added testing data to Claude error fallback");
7805 4907 }
7806 -
4908 +
7807 4909 header('Content-Type: application/json');
7808 4910 echo json_encode($response_data);
7809 4911 return true;
7810 4912 }
@@ -7810,29 +4912,9 @@
7810 4912 }
7811 4913
7812 4914 // Save the complete response to maintain chat persistence
7813 4915 if (!empty($full_response) && !empty($session_id)) {
7814 - // Prepare RAG context for streaming response
7815 - $rag_context_for_storage = null;
7816 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7817 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7818 -
7819 - if ($has_rag_data || $has_action_data) {
7820 - $rag_context_for_storage = [];
7821 -
7822 - if ($has_rag_data) {
7823 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7824 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7825 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7826 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7827 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7828 - }
7829 -
7830 - if ($has_action_data) {
7831 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7832 - }
7833 - }
7834 - $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);
7835 4917 }
7836 4918
7837 4919 return true; // Indicate streaming completed successfully
7838 4920
@@ -7837,9 +4919,9 @@
7837 4919 return true; // Indicate streaming completed successfully
7838 4920
7839 4921 } catch (Exception $e) {
7840 4922 //error_log("MxChat Claude streaming exception: " . $e->getMessage());
7841 -
4923 +
7842 4924 // Fallback to regular response on exception
7843 4925 $regular_response = $this->mxchat_generate_response_claude(
7844 4926 $selected_model,
7845 4927 $claude_api_key,
@@ -7845,35 +4927,20 @@
7845 4927 $claude_api_key,
7846 4928 $conversation_history,
7847 4929 $relevant_content
7848 4930 );
7849 -
7850 - // FIXED: Check if regular response returned an error
7851 - if (is_array($regular_response) && isset($regular_response['error'])) {
7852 - // Send error in SSE format since we're in streaming mode
7853 - echo "data: " . json_encode([
7854 - 'error' => true,
7855 - 'error_message' => $regular_response['error'],
7856 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7857 - 'text' => $regular_response['error'],
7858 - 'message' => $regular_response['error']
7859 - ]) . "\n\n";
7860 - echo "data: [DONE]\n\n";
7861 - flush();
7862 - return true;
7863 - }
7864 -
4931 +
7865 4932 $response_data = [
7866 4933 'text' => $regular_response,
7867 4934 'html' => '',
7868 4935 'session_id' => $session_id
7869 4936 ];
7870 -
4937 +
7871 4938 if ($testing_data !== null) {
7872 4939 $response_data['testing_data'] = $testing_data;
7873 4940 //error_log("MxChat Testing: Added testing data to Claude exception fallback");
7874 4941 }
7875 -
4942 +
7876 4943 header('Content-Type: application/json');
7877 4944 echo json_encode($response_data);
7878 4945 return true;
7879 4946 }
@@ -7883,9 +4950,9 @@
7883 4950 // Get bot ID from session or request
7884 4951 $bot_id = $this->get_current_bot_id($session_id);
7885 4952
7886 4953 // Get system prompt instructions using centralized function
7887 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
4954 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
7888 4955
7889 4956 // Ensure conversation_history is an array
7890 4957 if (!is_array($conversation_history)) {
7891 4958 $conversation_history = array();
@@ -7925,13 +4992,8 @@
7925 4992 $conversation_history,
7926 4993 $relevant_content
7927 4994 );
7928 4995
7929 - // Save bot response to transcript
7930 - if (!empty($regular_response) && !empty($session_id)) {
7931 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7932 - }
7933 -
7934 4996 $response_data = [
7935 4997 'text' => $regular_response,
7936 4998 'html' => '',
7937 4999 'session_id' => $session_id
@@ -7954,11 +5016,8 @@
7954 5016 'temperature' => 0.8,
7955 5017 'stream' => true
7956 5018 ]);
7957 5019
7958 - // Setup streaming headers now that we know we're actually streaming
7959 - $this->setup_streaming_headers();
7960 -
7961 5020 // Use cURL for streaming support
7962 5021 $ch = curl_init();
7963 5022 curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
7964 5023 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -8059,39 +5118,19 @@
8059 5118 return true;
8060 5119 }
8061 5120
8062 5121 curl_close($ch);
8063 -
5122 +
8064 5123 // Save the complete response to maintain chat persistence
8065 5124 if (!empty($full_response) && !empty($session_id)) {
8066 - // Prepare RAG context for streaming response
8067 - $rag_context_for_storage = null;
8068 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8069 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8070 -
8071 - if ($has_rag_data || $has_action_data) {
8072 - $rag_context_for_storage = [];
8073 -
8074 - if ($has_rag_data) {
8075 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8076 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8077 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8078 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8079 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8080 - }
8081 -
8082 - if ($has_action_data) {
8083 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8084 - }
8085 - }
8086 - $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);
8087 5126 }
8088 -
5127 +
8089 5128 return true; // Indicate streaming completed successfully
8090 -
5129 +
8091 5130 } catch (Exception $e) {
8092 5131 //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
8093 -
5132 +
8094 5133 // Fallback to regular response
8095 5134 $regular_response = $this->mxchat_generate_response_xai(
8096 5135 $selected_model,
8097 5136 $xai_api_key,
@@ -8120,9 +5159,9 @@
8120 5159 // Get bot ID from session or request
8121 5160 $bot_id = $this->get_current_bot_id($session_id);
8122 5161
8123 5162 // Get system prompt instructions using centralized function
8124 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5163 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8125 5164
8126 5165 // Ensure conversation_history is an array
8127 5166 if (!is_array($conversation_history)) {
8128 5167 $conversation_history = array();
@@ -8162,13 +5201,8 @@
8162 5201 $conversation_history,
8163 5202 $relevant_content
8164 5203 );
8165 5204
8166 - // Save bot response to transcript
8167 - if (!empty($regular_response) && !empty($session_id)) {
8168 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
8169 - }
8170 -
8171 5205 $response_data = [
8172 5206 'text' => $regular_response,
8173 5207 'html' => '',
8174 5208 'session_id' => $session_id
@@ -8191,11 +5225,8 @@
8191 5225 'temperature' => 0.8,
8192 5226 'stream' => true
8193 5227 ]);
8194 5228
8195 - // Setup streaming headers now that we know we're actually streaming
8196 - $this->setup_streaming_headers();
8197 -
8198 5229 // Use cURL for streaming support
8199 5230 $ch = curl_init();
8200 5231 curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
8201 5232 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -8310,39 +5341,19 @@
8310 5341 return true;
8311 5342 }
8312 5343
8313 5344 curl_close($ch);
8314 -
5345 +
8315 5346 // Save the complete response to maintain chat persistence
8316 5347 if (!empty($full_response) && !empty($session_id)) {
8317 - // Prepare RAG context for streaming response
8318 - $rag_context_for_storage = null;
8319 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8320 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8321 -
8322 - if ($has_rag_data || $has_action_data) {
8323 - $rag_context_for_storage = [];
8324 -
8325 - if ($has_rag_data) {
8326 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8327 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8328 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8329 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8330 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8331 - }
8332 -
8333 - if ($has_action_data) {
8334 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8335 - }
8336 - }
8337 - $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);
8338 5349 }
8339 -
5350 +
8340 5351 return true; // Indicate streaming completed successfully
8341 -
5352 +
8342 5353 } catch (Exception $e) {
8343 5354 //error_log("MxChat DeepSeek streaming exception: " . $e->getMessage());
8344 -
5355 +
8345 5356 // Fallback to regular response
8346 5357 $regular_response = $this->mxchat_generate_response_deepseek(
8347 5358 $selected_model,
8348 5359 $deepseek_api_key,
@@ -8376,112 +5387,8 @@
8376 5387 return true;
8377 5388 }
8378 5389 }
8379 5390
8380 -
8381 -private function mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content) {
8382 - try {
8383 - if (!is_array($conversation_history)) {
8384 - $conversation_history = array();
8385 - }
8386 -
8387 - $bot_id = $this->get_current_bot_id('');
8388 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8389 -
8390 - $formatted_conversation = array();
8391 -
8392 - $formatted_conversation[] = array(
8393 - 'role' => 'system',
8394 - 'content' => $system_prompt_instructions . " " . $relevant_content
8395 - );
8396 -
8397 - foreach ($conversation_history as $message) {
8398 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8399 - $role = $message['role'];
8400 -
8401 - if ($role === 'bot' || $role === 'agent') {
8402 - $role = 'assistant';
8403 - }
8404 - if (!in_array($role, ['system', 'assistant', 'user'])) {
8405 - $role = 'user';
8406 - }
8407 -
8408 - $formatted_conversation[] = array(
8409 - 'role' => $role,
8410 - 'content' => $message['content']
8411 - );
8412 - }
8413 - }
8414 -
8415 - $body = json_encode([
8416 - 'model' => $selected_model,
8417 - 'messages' => $formatted_conversation,
8418 - 'temperature' => 1,
8419 - ]);
8420 -
8421 - $args = [
8422 - 'body' => $body,
8423 - 'headers' => [
8424 - 'Content-Type' => 'application/json',
8425 - 'Authorization' => 'Bearer ' . $openrouter_api_key,
8426 - 'HTTP-Referer' => home_url(),
8427 - 'X-Title' => get_bloginfo('name'),
8428 - ],
8429 - 'timeout' => 60,
8430 - 'redirection' => 5,
8431 - 'blocking' => true,
8432 - 'httpversion' => '1.0',
8433 - 'sslverify' => true,
8434 - ];
8435 -
8436 - $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', $args);
8437 -
8438 - if (is_wp_error($response)) {
8439 - $error_message = $response->get_error_message();
8440 - return [
8441 - 'error' => esc_html__('Connection error when contacting OpenRouter: ', 'mxchat') . esc_html($error_message),
8442 - 'error_code' => 'openrouter_connection_error',
8443 - 'provider' => 'openrouter'
8444 - ];
8445 - }
8446 -
8447 - $status_code = wp_remote_retrieve_response_code($response);
8448 - if ($status_code !== 200) {
8449 - $response_body = wp_remote_retrieve_body($response);
8450 - $decoded_response = json_decode($response_body, true);
8451 -
8452 - $error_message = isset($decoded_response['error']['message'])
8453 - ? $decoded_response['error']['message']
8454 - : 'HTTP Error ' . $status_code;
8455 -
8456 - return [
8457 - 'error' => esc_html__('OpenRouter API error: ', 'mxchat') . esc_html($error_message),
8458 - 'error_code' => 'openrouter_api_error',
8459 - 'provider' => 'openrouter',
8460 - 'status_code' => $status_code
8461 - ];
8462 - }
8463 -
8464 - $response_body = wp_remote_retrieve_body($response);
8465 - $decoded_response = json_decode($response_body, true);
8466 -
8467 - if (isset($decoded_response['choices'][0]['message']['content'])) {
8468 - return trim($decoded_response['choices'][0]['message']['content']);
8469 - } else {
8470 - return [
8471 - 'error' => esc_html__('Unexpected response format from OpenRouter.', 'mxchat'),
8472 - 'error_code' => 'openrouter_response_format_error',
8473 - 'provider' => 'openrouter'
8474 - ];
8475 - }
8476 - } catch (Exception $e) {
8477 - return [
8478 - 'error' => esc_html__('System error when processing OpenRouter request: ', 'mxchat') . esc_html($e->getMessage()),
8479 - 'error_code' => 'openrouter_exception',
8480 - 'provider' => 'openrouter'
8481 - ];
8482 - }
8483 -}
8484 5391 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
8485 5392
8486 5393 // Get bot ID from session or request
8487 5394 $bot_id = $this->get_current_bot_id($session_id);
@@ -8486,9 +5393,9 @@
8486 5393 // Get bot ID from session or request
8487 5394 $bot_id = $this->get_current_bot_id($session_id);
8488 5395
8489 5396 // Get system prompt instructions using centralized function
8490 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5397 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8491 5398
8492 5399 // Clean and validate conversation history
8493 5400 foreach ($conversation_history as &$message) {
8494 5401 // Convert bot and agent roles to assistant
@@ -8592,12 +5499,12 @@
8592 5499 $conversation_history = array();
8593 5500 }
8594 5501
8595 5502 // Get bot ID from session or request
8596 - $bot_id = $this->get_current_bot_id('');
5503 + $bot_id = $this->get_current_bot_id($session_id);
8597 5504
8598 5505 // Get system prompt instructions using centralized function
8599 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5506 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8600 5507
8601 5508 // Create a new array for the formatted conversation
8602 5509 $formatted_conversation = array();
8603 5510
@@ -8626,42 +5533,15 @@
8626 5533 );
8627 5534 }
8628 5535 }
8629 5536
8630 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
8631 - $is_gpt5_model = (
8632 - strpos($selected_model, 'gpt-5') === 0 ||
8633 - $selected_model === 'gpt-5.2' ||
8634 - $selected_model === 'gpt-5.1-2025-11-13' ||
8635 - $selected_model === 'gpt-5' ||
8636 - $selected_model === 'gpt-5-mini' ||
8637 - $selected_model === 'gpt-5-nano'
8638 - );
8639 -
8640 - // Build request body with optimal settings for fast responses
8641 - $request_body = [
5537 + $body = json_encode([
8642 5538 'model' => $selected_model,
8643 5539 'messages' => $formatted_conversation,
8644 5540 'temperature' => 1,
8645 5541 'stream' => false
8646 - ];
5542 + ]);
8647 5543
8648 - // Add reasoning_effort only for GPT-5 models that support it
8649 - // These chat models don't support reasoning_effort parameter
8650 - $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');
8651 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
8652 - // GPT-5.1 uses 'low' instead of 'minimal'
8653 - if ($selected_model === 'gpt-5.1-2025-11-13') {
8654 - $request_body['reasoning_effort'] = 'low';
8655 - } elseif ($selected_model === 'gpt-5.4') {
8656 - $request_body['reasoning_effort'] = 'none';
8657 - } else {
8658 - $request_body['reasoning_effort'] = 'minimal';
8659 - }
8660 - }
8661 -
8662 - $body = json_encode($request_body);
8663 -
8664 5544 $args = [
8665 5545 'body' => $body,
8666 5546 'headers' => [
8667 5547 'Content-Type' => 'application/json',
@@ -8677,8 +5557,9 @@
8677 5557 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
8678 5558
8679 5559 if (is_wp_error($response)) {
8680 5560 $error_message = $response->get_error_message();
5561 + //error_log('OpenAI API Error: ' . $error_message);
8681 5562 return [
8682 5563 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
8683 5564 'error_code' => 'openai_connection_error',
8684 5565 'provider' => 'openai'
@@ -8697,8 +5578,10 @@
8697 5578 $error_type = isset($decoded_response['error']['type'])
8698 5579 ? $decoded_response['error']['type']
8699 5580 : 'unknown';
8700 5581
5582 + //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
5583 +
8701 5584 // Handle specific error types
8702 5585 switch ($error_type) {
8703 5586 case 'invalid_request_error':
8704 5587 if (strpos($error_message, 'API key') !== false) {
@@ -8746,8 +5629,9 @@
8746 5629
8747 5630 if (isset($decoded_response['choices'][0]['message']['content'])) {
8748 5631 return trim($decoded_response['choices'][0]['message']['content']);
8749 5632 } else {
5633 + //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
8750 5634 return [
8751 5635 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
8752 5636 'error_code' => 'openai_response_format_error',
8753 5637 'provider' => 'openai'
@@ -8753,8 +5637,9 @@
8753 5637 'provider' => 'openai'
8754 5638 ];
8755 5639 }
8756 5640 } catch (Exception $e) {
5641 + //error_log('OpenAI Exception: ' . $e->getMessage());
8757 5642 return [
8758 5643 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
8759 5644 'error_code' => 'openai_exception',
8760 5645 'provider' => 'openai'
@@ -8760,9 +5645,8 @@
8760 5645 'provider' => 'openai'
8761 5646 ];
8762 5647 }
8763 5648 }
8764 -
8765 5649 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
8766 5650 try {
8767 5651 // Get bot ID from session or request
8768 5652 $bot_id = $this->get_current_bot_id($session_id);
@@ -8767,9 +5651,9 @@
8767 5651 // Get bot ID from session or request
8768 5652 $bot_id = $this->get_current_bot_id($session_id);
8769 5653
8770 5654 // Get system prompt instructions using centralized function
8771 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5655 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8772 5656
8773 5657 // Add system prompt to relevant content
8774 5658 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
8775 5659
@@ -8966,9 +5850,9 @@
8966 5850 // Get bot ID from session or request
8967 5851 $bot_id = $this->get_current_bot_id($session_id);
8968 5852
8969 5853 // Get system prompt instructions using centralized function
8970 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5854 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8971 5855
8972 5856 // Create a new array for the formatted conversation
8973 5857 $formatted_conversation = array();
8974 5858
@@ -9125,9 +6009,9 @@
9125 6009 // Get bot ID from session or request
9126 6010 $bot_id = $this->get_current_bot_id($session_id);
9127 6011
9128 6012 // Get system prompt instructions using centralized function
9129 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6013 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
9130 6014
9131 6015 // Add system prompt to relevant content
9132 6016 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
9133 6017
@@ -9223,11 +6107,9 @@
9223 6107 ]
9224 6108 ]);
9225 6109
9226 6110 // Prepare the API endpoint
9227 - // Use v1beta for preview models (Gemini 3, experimental), v1 for stable models
9228 - $api_version = (strpos($selected_model, 'preview') !== false || strpos($selected_model, 'exp') !== false) ? 'v1beta' : 'v1';
9229 - $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;
9230 6112
9231 6113 // Set up the API request
9232 6114 $args = [
9233 6115 'body' => $body,
@@ -9269,9 +6151,9 @@
9269 6151
9270 6152
9271 6153 public function test_streaming_request() {
9272 6154 $options = get_option('mxchat_options', []);
9273 - $model = $options['model'] ?? 'gpt-5.1-chat-latest';
6155 + $model = $options['model'] ?? 'gpt-4o';
9274 6156
9275 6157 // Detect provider from model prefix
9276 6158 $provider = strtolower(explode('-', $model)[0]);
9277 6159
@@ -9454,54 +6336,35 @@
9454 6336 }
9455 6337
9456 6338
9457 6339 public function mxchat_enqueue_scripts_styles() {
9458 - // Fetch options from the database first to check loading strategy
9459 - $this->options = get_option('mxchat_options');
9460 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
9461 -
9462 - // 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
9463 6352 wp_enqueue_style(
9464 6353 'mxchat-chat-css',
9465 6354 plugin_dir_url(__FILE__) . '../css/chat-style.css',
9466 6355 array(),
9467 - MXCHAT_VERSION
6356 + $chat_style_version
9468 6357 );
9469 -
9470 - // Handle script loading based on strategy
9471 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
9472 - // Enqueue the script normally
9473 - wp_enqueue_script(
9474 - 'mxchat-chat-js',
9475 - plugin_dir_url(__FILE__) . '../js/chat-script.js',
9476 - array('jquery'),
9477 - MXCHAT_VERSION,
9478 - true
9479 - );
9480 -
9481 - // Add defer attribute if strategy is 'defer'
9482 - if ($loading_strategy === 'defer') {
9483 - wp_script_add_data('mxchat-chat-js', 'strategy', 'defer');
9484 - }
9485 - } else {
9486 - // For delay or interaction-based loading, we'll use a custom loader
9487 - // Don't enqueue the main script - we'll load it dynamically
9488 - add_action('wp_footer', array($this, 'mxchat_output_delayed_script_loader'), 99);
9489 - }
9490 -
6358 + // Fetch options from the database
6359 + $this->options = get_option('mxchat_options');
9491 6360 $prompts_options = get_option('mxchat_prompts_options', array());
9492 -
9493 - // Check if AI theme is active - if so, skip inline colors in JavaScript
9494 - $theme_options = get_option('mxchat_theme_options', array());
9495 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9496 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9497 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9498 -
6361 +
9499 6362 // Prepare settings for JavaScript
9500 6363 $style_settings = array(
9501 6364 'ajax_url' => admin_url('admin-ajax.php'),
9502 6365 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9503 - 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
6366 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
9504 6367 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
9505 6368 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
9506 6369 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
9507 6370 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
@@ -9528,134 +6391,15 @@
9528 6391 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9529 6392 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off', // FIXED
9530 6393 'initial_email_state' => null, // Also fixed this undefined variable
9531 6394 'skip_email_check' => true,
9532 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9533 - 'skip_inline_colors' => $skip_inline_colors,
9534 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
6395 + 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
9535 6396 );
9536 -
9537 - // For normal/defer loading, use wp_localize_script
9538 - // For delayed loading, we store settings in a transient to be output inline
9539 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
9540 - wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
9541 - } else {
9542 - // Store settings for the delayed loader to use
9543 - set_transient('mxchat_delayed_settings_' . get_current_user_id(), $style_settings, 60);
9544 - }
6397 + // Pass the settings to the script
6398 + wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
9545 6399 }
9546 6400
9547 -/**
9548 - * Output the delayed script loader for performance optimization
9549 - */
9550 -public function mxchat_output_delayed_script_loader() {
9551 - $this->options = get_option('mxchat_options');
9552 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
9553 - $script_url = plugin_dir_url(__FILE__) . '../js/chat-script.js?ver=' . MXCHAT_VERSION;
9554 6401
9555 - // Get the stored settings
9556 - $prompts_options = get_option('mxchat_prompts_options', array());
9557 - $theme_options = get_option('mxchat_theme_options', array());
9558 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9559 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9560 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9561 -
9562 - $style_settings = array(
9563 - 'ajax_url' => admin_url('admin-ajax.php'),
9564 - 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9565 - 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
9566 - 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
9567 - 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
9568 - 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
9569 - 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
9570 - 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
9571 - 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
9572 - 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
9573 - 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
9574 - 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
9575 - 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
9576 - 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
9577 - 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
9578 - 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
9579 - 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
9580 - 'icon_color' => $this->options['icon_color'] ?? '#fff',
9581 - 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
9582 - 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
9583 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
9584 - 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
9585 - 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
9586 - 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
9587 - 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
9588 - 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
9589 - 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
9590 - 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9591 - 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off',
9592 - 'initial_email_state' => null,
9593 - 'skip_email_check' => true,
9594 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9595 - 'skip_inline_colors' => $skip_inline_colors,
9596 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
9597 - );
9598 -
9599 - // Determine delay time based on strategy
9600 - $delay_ms = 0;
9601 - switch ($loading_strategy) {
9602 - case 'delay_1s':
9603 - $delay_ms = 1000;
9604 - break;
9605 - case 'delay_3s':
9606 - $delay_ms = 3000;
9607 - break;
9608 - case 'delay_5s':
9609 - $delay_ms = 5000;
9610 - break;
9611 - }
9612 -
9613 - ?>
9614 - <script type="text/javascript">
9615 - (function() {
9616 - var mxchatLoaded = false;
9617 - var mxchatChat = <?php echo wp_json_encode($style_settings); ?>;
9618 - window.mxchatChat = mxchatChat;
9619 -
9620 - function loadMxChatScript() {
9621 - if (mxchatLoaded) return;
9622 - mxchatLoaded = true;
9623 -
9624 - function appendChatScript() {
9625 - var script = document.createElement('script');
9626 - script.src = <?php echo wp_json_encode($script_url); ?>;
9627 - script.type = 'text/javascript';
9628 - document.body.appendChild(script);
9629 - }
9630 -
9631 - if (typeof jQuery !== 'undefined') {
9632 - appendChatScript();
9633 - } else {
9634 - var jq = document.createElement('script');
9635 - jq.src = <?php echo wp_json_encode(includes_url('js/jquery/jquery.min.js')); ?>;
9636 - jq.onload = appendChatScript;
9637 - document.body.appendChild(jq);
9638 - }
9639 - }
9640 -
9641 - <?php if ($loading_strategy === 'on_interaction'): ?>
9642 - // Load on user interaction
9643 - var events = ['scroll', 'mousemove', 'touchstart', 'keydown', 'click'];
9644 - events.forEach(function(evt) {
9645 - window.addEventListener(evt, loadMxChatScript, {once: true, passive: true});
9646 - });
9647 - // Fallback: load after 8 seconds if no interaction
9648 - setTimeout(loadMxChatScript, 8000);
9649 - <?php else: ?>
9650 - // Load after specified delay
9651 - setTimeout(loadMxChatScript, <?php echo intval($delay_ms); ?>);
9652 - <?php endif; ?>
9653 - })();
9654 - </script>
9655 - <?php
9656 -}
9657 -
9658 6402 /**
9659 6403 * Setup the cron jobs for rate limits with guard against multiple calls
9660 6404 */
9661 6405 public function setup_rate_limit_cron_jobs() {
@@ -10190,11 +6934,8 @@
10190 6934
10191 6935 /**
10192 6936 * AJAX handler to get system information for testing panel
10193 6937 */
10194 -/**
10195 - * AJAX handler to get system information for testing panel
10196 - */
10197 6938 public function mxchat_get_system_info() {
10198 6939 // Verify nonce for security
10199 6940 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10200 6941 wp_send_json_error(['message' => 'Invalid nonce']);
@@ -10211,25 +6952,11 @@
10211 6952 $system_prompt = isset($this->options['system_prompt_instructions'])
10212 6953 ? $this->options['system_prompt_instructions']
10213 6954 : 'No system prompt configured';
10214 6955
10215 - // Get selected model
10216 - $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';
10217 6958
10218 - // Check if OpenRouter is being used
10219 - $is_openrouter = ($selected_model === 'openrouter');
10220 - $openrouter_model = '';
10221 -
10222 - if ($is_openrouter) {
10223 - // Get the actual OpenRouter model that's selected
10224 - $openrouter_model = isset($this->options['openrouter_selected_model'])
10225 - ? $this->options['openrouter_selected_model']
10226 - : 'No OpenRouter model selected';
10227 -
10228 - // Update selected_model display to show both
10229 - $selected_model = 'OpenRouter: ' . $openrouter_model;
10230 - }
10231 -
10232 6959 // Get API key status (just check if they exist, don't expose the keys)
10233 6960 $api_status = [];
10234 6961 $api_status['openai'] = !empty($this->options['api_key']);
10235 6962 $api_status['claude'] = !empty($this->options['claude_api_key']);
@@ -10235,15 +6962,12 @@
10235 6962 $api_status['claude'] = !empty($this->options['claude_api_key']);
10236 6963 $api_status['gemini'] = !empty($this->options['gemini_api_key']);
10237 6964 $api_status['xai'] = !empty($this->options['xai_api_key']);
10238 6965 $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
10239 - $api_status['openrouter'] = !empty($this->options['openrouter_api_key']);
10240 6966
10241 6967 wp_send_json_success([
10242 6968 'system_prompt' => $system_prompt,
10243 6969 'selected_model' => $selected_model,
10244 - 'is_openrouter' => $is_openrouter,
10245 - 'openrouter_model' => $openrouter_model,
10246 6970 'api_status' => $api_status
10247 6971 ]);
10248 6972 }
10249 6973
@@ -10282,42 +7006,24 @@
10282 7006 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10283 7007 wp_send_json_error(['message' => 'Invalid nonce']);
10284 7008 return;
10285 7009 }
10286 -
7010 +
10287 7011 // Only allow admin users
10288 7012 if (!current_user_can('administrator')) {
10289 7013 wp_send_json_error(['message' => 'Unauthorized']);
10290 7014 return;
10291 7015 }
10292 -
10293 - // Check OpenAI Vector Store first (takes priority)
10294 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
10295 - $use_vectorstore = (isset($vectorstore_options['mxchat_use_openai_vectorstore']) && $vectorstore_options['mxchat_use_openai_vectorstore'] === '1');
10296 -
10297 - if ($use_vectorstore) {
10298 - $vectorstore_ids = $vectorstore_options['mxchat_openai_vectorstore_ids'] ?? '';
10299 - $id_count = !empty($vectorstore_ids) ? count(array_filter(array_map('trim', explode(',', $vectorstore_ids)))) : 0;
10300 -
10301 - $kb_info = [
10302 - 'type' => 'OpenAI Vector Store',
10303 - 'status' => 'Active',
10304 - 'documents' => $id_count > 0 ? $id_count . ' vector store' . ($id_count > 1 ? 's' : '') . ' configured' : 'No vector stores configured'
10305 - ];
10306 -
10307 - wp_send_json_success($kb_info);
10308 - return;
10309 - }
10310 -
7016 +
10311 7017 // Check Pinecone vs WordPress
10312 7018 $addon_options = get_option('mxchat_pinecone_addon_options', array());
10313 7019 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
10314 -
7020 +
10315 7021 $kb_info = [
10316 7022 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
10317 7023 'status' => 'Active'
10318 7024 ];
10319 -
7025 +
10320 7026 // Get document count
10321 7027 if ($use_pinecone) {
10322 7028 $kb_info['documents'] = 'Connected to Pinecone';
10323 7029 $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
@@ -10327,9 +7033,9 @@
10327 7033 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
10328 7034 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
10329 7035 $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
10330 7036 }
10331 -
7037 +
10332 7038 wp_send_json_success($kb_info);
10333 7039 }
10334 7040
10335 7041 /**
@@ -10411,13 +7117,9 @@
10411 7117 // Clear any other session-specific transients
10412 7118 delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
10413 7119 delete_transient("mxchat_include_pdf_in_context_{$session_id}");
10414 7120 delete_transient("mxchat_include_word_in_context_{$session_id}");
10415 -
10416 - // Clear form addon state (pending forms and submitted forms)
10417 - delete_option("mxchat_pending_form_{$session_id}");
10418 - delete_option("mxchat_submitted_forms_{$session_id}");
10419 -
7121 +
10420 7122 //error_log("MxChat: Cleared all data for session: {$session_id}");
10421 7123 }
10422 7124
10423 7125 /**
@@ -10592,169 +7294,8 @@
10592 7294 wp_send_json_success(['message' => 'Originating page tracked']);
10593 7295 wp_die();
10594 7296 }
10595 7297
10596 -/**
10597 - * Validate and clean URLs from AI response
10598 - * Removes any URLs that aren't in the knowledge base
10599 - *
10600 - * @param string $response_text The AI-generated response
10601 - * @param array $valid_urls Array of URLs from the knowledge base
10602 - * @return string Cleaned response with invalid URLs removed/flagged
10603 - */
10604 -private function validate_and_clean_urls($response_text, $valid_urls) {
10605 - // DEBUG: Log what we're working with
10606 - //error_log("=== MxChat URL Validation Debug ===");
10607 - //error_log("Valid URLs count: " . count($valid_urls));
10608 - //error_log("Valid URLs: " . print_r($valid_urls, true));
10609 - //error_log("Response text length: " . strlen($response_text));
10610 - //error_log("Response text preview: " . substr($response_text, 0, 500));
10611 -
10612 - // If no valid URLs provided or empty response, return as-is
10613 - if (empty($valid_urls) || empty($response_text)) {
10614 - //error_log("Validation skipped - empty valid_urls or response");
10615 - return $response_text;
10616 - }
10617 -
10618 - // Extract all URLs from the AI response
10619 - // This regex matches http:// and https:// URLs
10620 - preg_match_all(
10621 - '#\bhttps?://[^\s<>"\')\]]+#i',
10622 - $response_text,
10623 - $matches
10624 - );
10625 -
10626 - // If no URLs found in response, return as-is
10627 - if (empty($matches[0])) {
10628 - //error_log("No URLs found in response");
10629 - return $response_text;
10630 - }
10631 -
10632 - $found_urls = $matches[0];
10633 - $cleaned_response = $response_text;
10634 - $removed_count = 0;
10635 -
10636 - // Normalize valid URLs for comparison (remove trailing slashes, fragments, etc.)
10637 - $normalized_valid_urls = array_map(function($url) {
10638 - // Remove trailing slash
10639 - $url = rtrim($url, '/');
10640 - // Remove URL fragments (#section)
10641 - $url = preg_replace('/#.*$/', '', $url);
10642 - // Remove trailing punctuation that might have been captured
10643 - $url = rtrim($url, '.,;:!?');
10644 - return $url;
10645 - }, $valid_urls);
10646 -
10647 - //error_log("Normalized valid URLs: " . print_r($normalized_valid_urls, true));
10648 -
10649 - foreach ($found_urls as $found_url) {
10650 - // Clean up the found URL (remove trailing punctuation that might have been captured)
10651 - $clean_found_url = rtrim($found_url, '.,;:!?)');
10652 -
10653 - // DEBUG: Log each URL being checked
10654 - //error_log("Checking found URL: " . $found_url);
10655 -
10656 - // Normalize for comparison
10657 - $normalized_found = rtrim($clean_found_url, '/');
10658 - $normalized_found = preg_replace('/#.*$/', '', $normalized_found);
10659 -
10660 - //error_log("Normalized found URL: " . $normalized_found);
10661 -
10662 - // Check if this URL exists in our valid URLs list
10663 - $is_valid = false;
10664 -
10665 - //error_log("Starting validation checks for: " . $normalized_found);
10666 -
10667 - // First, try exact match
10668 - if (in_array($normalized_found, $normalized_valid_urls)) {
10669 - $is_valid = true;
10670 - //error_log("EXACT MATCH FOUND");
10671 - } else {
10672 - //error_log("No exact match, checking variations...");
10673 - // If no exact match, check if it's a variation (with query params, etc.)
10674 - foreach ($normalized_valid_urls as $valid_url) {
10675 - //error_log(" Comparing against valid URL: " . $valid_url);
10676 -
10677 - // Check if the found URL starts with a valid URL (handles query params)
10678 - if (strpos($normalized_found, $valid_url) === 0) {
10679 - // Check what comes after the valid URL
10680 - $remainder = substr($normalized_found, strlen($valid_url));
10681 -
10682 - // Only valid if:
10683 - // 1. Exact match (remainder is empty)
10684 - // 2. Query params (starts with ?)
10685 - // 3. Fragment (starts with #)
10686 - if (empty($remainder) || $remainder[0] === '?' || $remainder[0] === '#') {
10687 - $is_valid = true;
10688 - //error_log(" MATCH: Found URL is valid variation of base URL");
10689 - break;
10690 - } else {
10691 - //error_log(" NOT A MATCH: Found URL extends path beyond valid URL (remainder: " . $remainder . ")");
10692 - }
10693 - }
10694 - // Also check the reverse (in case valid URL has query params)
10695 - if (strpos($valid_url, $normalized_found) === 0) {
10696 - $is_valid = true;
10697 - //error_log(" MATCH: Valid URL starts with found URL");
10698 - break;
10699 - }
10700 - }
10701 -
10702 - if (!$is_valid) {
10703 - //error_log("NO MATCH FOUND - URL should be removed");
10704 - }
10705 - }
10706 -
10707 - // If URL is not valid, remove it from the response
10708 - if (!$is_valid) {
10709 - // Log the removal for debugging
10710 - //error_log("MxChat: Removed hallucinated URL: " . $found_url);
10711 - //error_log("MxChat: Valid URLs were: " . implode(', ', array_slice($normalized_valid_urls, 0, 5)));
10712 -
10713 - $removed_count++;
10714 -
10715 - // Check if URL is part of a markdown link: [text](url)
10716 - $markdown_pattern = '/\[([^\]]+)\]\(' . preg_quote($found_url, '/') . '\)/';
10717 - if (preg_match($markdown_pattern, $cleaned_response)) {
10718 - //error_log("Found markdown link, removing but keeping text");
10719 - // Remove the markdown link but keep the text
10720 - $cleaned_response = preg_replace($markdown_pattern, '$1', $cleaned_response);
10721 - }
10722 - // Check if URL is part of an HTML link: <a href="url">text</a>
10723 - else if (preg_match('/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>(.*?)<\/a>/i', $cleaned_response, $link_match)) {
10724 - //error_log("Found HTML link, removing but keeping text");
10725 - // Remove the HTML link but keep the text
10726 - $link_text = $link_match[1];
10727 - $cleaned_response = preg_replace(
10728 - '/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>.*?<\/a>/i',
10729 - $link_text,
10730 - $cleaned_response
10731 - );
10732 - }
10733 - // Otherwise just remove the bare URL
10734 - else {
10735 - //error_log("Removing bare URL");
10736 - $cleaned_response = str_replace($found_url, '', $cleaned_response);
10737 - }
10738 - }
10739 - }
10740 -
10741 - // Log summary if any URLs were removed
10742 - if ($removed_count > 0) {
10743 - //error_log("MxChat: URL Validation Summary - Removed {$removed_count} hallucinated URL(s)");
10744 - } else {
10745 - //error_log("MxChat: URL Validation Summary - No URLs removed, all were valid");
10746 - }
10747 -
10748 - // Clean up any double spaces or awkward punctuation left behind
10749 - // IMPORTANT: Only collapse horizontal whitespace (spaces/tabs), preserve newlines for markdown formatting
10750 - $cleaned_response = preg_replace('/[^\S\n]+/', ' ', $cleaned_response); // Collapse spaces/tabs but NOT newlines
10751 - $cleaned_response = preg_replace('/[^\S\n]+([.,;:!?])/', '$1', $cleaned_response); // Same for punctuation cleanup
10752 -
10753 - //error_log("Final cleaned response: " . $cleaned_response);
10754 -
10755 - return trim($cleaned_response);
10756 -}
10757 7298
10758 7299 /**
10759 7300 * AJAX handler to get current chat mode for a session
10760 7301 */