PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.4.4
MxChat – AI Chatbot & Content Generation for WordPress v2.4.4
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 +604 -4543 3.2.62.4.4 View file →
@@ -10,40 +10,10 @@
10 10 private $fallbackResponse;
11 11 private $productCardHtml;
12 12 private $word_handler;
13 13 private $last_similarity_analysis = null;
14 - private $current_valid_urls = [];
15 - private $last_vectorstore_error = null;
16 - private $is_streaming = false; // ADDED: Track if current request is streaming
17 - private $streaming_headers_sent = false; // Track if streaming headers have been sent
18 14
19 -/**
20 - * Setup streaming headers - call this right before actually streaming
21 - * This delays header setup to allow actions/forms to return JSON responses
22 - */
23 -private function setup_streaming_headers() {
24 - if ($this->streaming_headers_sent || headers_sent()) {
25 - return false;
26 - }
27 15
28 - // Disable output buffering
29 - while (ob_get_level()) {
30 - ob_end_flush();
31 - }
32 -
33 - // Set headers for SSE
34 - header('Content-Type: text/event-stream');
35 - header('Cache-Control: no-cache');
36 - header('Connection: keep-alive');
37 - header('X-Accel-Buffering: no');
38 -
39 - ob_implicit_flush(true);
40 - flush();
41 -
42 - $this->streaming_headers_sent = true;
43 - return true;
44 -}
45 -
46 16 /**
47 17 * Class constructor
48 18 */
49 19 public function __construct() {
@@ -111,28 +81,13 @@
111 81 // Add chat mode checking actions
112 82 add_action('wp_ajax_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
113 83 add_action('wp_ajax_nopriv_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
114 84
115 - // Nonce refresh for page-cache compatibility (WP Rocket, LiteSpeed, etc.)
116 - add_action('wp_ajax_mxchat_refresh_nonce', array($this, 'mxchat_refresh_nonce'));
117 - add_action('wp_ajax_nopriv_mxchat_refresh_nonce', array($this, 'mxchat_refresh_nonce'));
118 -
119 - // Auto-email transcript action
120 - add_action('mxchat_send_delayed_transcript', array($this, 'mxchat_send_delayed_transcript'), 10, 1);
121 -
122 85 add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
123 86
124 87
125 88 }
126 89
127 -/**
128 - * Return a fresh nonce so cached pages can replace the stale one.
129 - */
130 -public function mxchat_refresh_nonce() {
131 - nocache_headers();
132 - wp_send_json_success(array('nonce' => wp_create_nonce('mxchat_chat_nonce')));
133 -}
134 -
135 90 // In your core plugin's check_actions_for_addons method:
136 91 public function check_actions_for_addons($default, $message, $user_id, $session_id) {
137 92 //error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
138 93
@@ -155,22 +110,8 @@
155 110 wp_die();
156 111 }
157 112
158 113 $session_id = sanitize_text_field($_POST['session_id']);
159 -
160 - // SECURITY FIX: Verify session ownership before retrieving data
161 - // If IP/user changed, signal frontend to reset session instead of blocking
162 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
163 -
164 - // Check if this session has an owner recorded
165 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
166 -
167 - // Update session owner if it changed (e.g. IP changed due to network switch)
168 - // The session ID itself is the authentication — if the client has it, they own it
169 - if (!$session_owner || $session_owner !== $current_user_identifier) {
170 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
171 - }
172 -
173 114 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
174 115 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
175 116
176 117 if (empty($history)) {
@@ -187,25 +128,11 @@
187 128 'chat_mode' => $chat_mode
188 129 ]);
189 130 wp_die();
190 131 }
191 -private function mxchat_fetch_conversation_history_for_ai($session_id, $session_start_timestamp = 0) {
132 +
133 +private function mxchat_fetch_conversation_history_for_ai($session_id) {
192 134 $history = get_option("mxchat_history_{$session_id}", []);
193 -
194 - // Check persistence setting - when OFF, only include messages from current page load
195 - $options = get_option('mxchat_options', []);
196 - $persistence_enabled = isset($options['chat_persistence_toggle']) && $options['chat_persistence_toggle'] === 'on';
197 -
198 - // Filter history when persistence is OFF to match what the user sees
199 - if (!$persistence_enabled && $session_start_timestamp > 0) {
200 - $history = array_filter($history, function($entry) use ($session_start_timestamp) {
201 - // Include messages from this page load onwards
202 - return isset($entry['timestamp']) && $entry['timestamp'] >= $session_start_timestamp;
203 - });
204 - // Re-index array after filtering
205 - $history = array_values($history);
206 - }
207 -
208 135 $formatted_history = [];
209 136
210 137 // Adjusted for code-heavy conversations
211 138 $max_tokens = 120000; // Context window size
@@ -303,15 +230,8 @@
303 230 'callback' => [$this, 'handle_slack_messages'],
304 231 'permission_callback' => [$this, 'verify_slack_request'],
305 232 ]);
306 233
307 - // Telegram webhook endpoint
308 - register_rest_route('mxchat/v1', '/telegram-webhook', [
309 - 'methods' => 'POST',
310 - 'callback' => [$this, 'handle_telegram_webhook'],
311 - 'permission_callback' => [$this, 'verify_telegram_request'],
312 - ]);
313 -
314 234 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
315 235 }
316 236
317 237 /**
@@ -351,11 +271,10 @@
351 271 //error_log(esc_html__('Slack request timestamp too old', 'mxchat'));
352 272 return false;
353 273 }
354 274
355 - // Get raw request body from the WP_REST_Request object
356 - // (php://input may already be consumed by WordPress at this point)
357 - $request_body = $request->get_body();
275 + // Get raw request body
276 + $request_body = file_get_contents('php://input');
358 277
359 278 // Create the signature base string
360 279 $sig_basestring = "v0:{$timestamp}:{$request_body}";
361 280
@@ -364,43 +283,8 @@
364 283
365 284 // Compare signatures
366 285 return hash_equals($my_signature, $slack_signature);
367 286 }
368 -
369 -/**
370 - * Verify request is coming from Telegram.
371 - *
372 - * @param WP_REST_Request $request
373 - * @return bool True if valid, false otherwise.
374 - */
375 -public function verify_telegram_request($request) {
376 - $secret_token = $this->options['telegram_webhook_secret'] ?? '';
377 -
378 - //error_log('[MxChat Telegram DEBUG] verify_telegram_request called');
379 - //error_log('[MxChat Telegram DEBUG] Stored secret: ' . (empty($secret_token) ? 'EMPTY' : substr($secret_token, 0, 10) . '...'));
380 -
381 - if (empty($secret_token)) {
382 - // If no secret is configured, allow the request (for initial setup)
383 - //error_log('[MxChat Telegram DEBUG] No secret configured, allowing request');
384 - return true;
385 - }
386 -
387 - // Telegram sends the secret token in the X-Telegram-Bot-Api-Secret-Token header
388 - $request_token = $request->get_header('X-Telegram-Bot-Api-Secret-Token');
389 -
390 - //error_log('[MxChat Telegram DEBUG] Request token: ' . (empty($request_token) ? 'EMPTY' : substr($request_token, 0, 10) . '...'));
391 -
392 - if (empty($request_token)) {
393 - //error_log('[MxChat Telegram DEBUG] Request rejected: No token in header');
394 - return false;
395 - }
396 -
397 - // Timing-safe comparison
398 - $result = hash_equals($secret_token, $request_token);
399 - //error_log('[MxChat Telegram DEBUG] Token comparison result: ' . ($result ? 'MATCH' : 'MISMATCH'));
400 - return $result;
401 -}
402 -
403 287 public function mxchat_stream_events(WP_REST_Request $request) {
404 288 header('Content-Type: text/event-stream');
405 289 header('Cache-Control: no-cache');
406 290 header('Connection: keep-alive');
@@ -434,9 +318,9 @@
434 318
435 319
436 320
437 321
438 -private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null, $rag_context = null) {
322 +private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null) {
439 323 global $wpdb;
440 324 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
441 325 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
442 326
@@ -454,20 +338,8 @@
454 338 //error_log("[DEBUG] This is a NEW session - first message");
455 339 }
456 340 }
457 341
458 - // SECURITY FIX: Set session ownership for new sessions
459 - if ($is_new_session && $role === 'user') {
460 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
461 - $session_owner_key = "mxchat_session_owner_{$session_id}";
462 -
463 - // Only set ownership if not already set
464 - if (!get_option($session_owner_key)) {
465 - update_option($session_owner_key, $current_user_identifier, 'no');
466 - //error_log("[DEBUG] Set session ownership for {$session_id} to {$current_user_identifier}");
467 - }
468 - }
469 -
470 342 // 1) Extract agent name if present
471 343 $agent_name = '';
472 344 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
473 345 $agent_name = $matches[1];
@@ -610,17 +482,9 @@
610 482 $insert_data['originating_page_title'] = $stored_originating['title'] ?? '';
611 483 }
612 484 }
613 485 }
614 -
615 - // Add RAG context if provided (for bot messages)
616 - if ($rag_context !== null && $role === 'bot') {
617 - $rag_context_column_exists = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'rag_context'");
618 - if ($rag_context_column_exists) {
619 - $insert_data['rag_context'] = is_array($rag_context) ? wp_json_encode($rag_context) : $rag_context;
620 - }
621 - }
622 -
486 +
623 487 $wpdb->insert($table_name, $insert_data);
624 488 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
625 489
626 490 // 9) Send notification email if this is the first user message in a new session
@@ -631,17 +495,11 @@
631 495 'ip' => $_SERVER['REMOTE_ADDR']
632 496 ));
633 497 }
634 498
635 - // 10) Schedule delayed transcript email if enabled and message is from user
636 - if ($wpdb->insert_id && $role === 'user') {
637 - $this->schedule_delayed_transcript_email($session_id);
638 - }
639 -
640 499 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
641 500 return $message_id;
642 501 }
643 -
644 502 private function send_new_chat_notification($session_id, $user_info = array()) {
645 503 $options = get_option('mxchat_transcripts_options');
646 504
647 505 // Check if notifications are enabled
@@ -684,200 +542,12 @@
684 542 // Send email
685 543 return wp_mail($to, $subject, $message);
686 544 }
687 545
688 -/**
689 - * Schedule delayed transcript email for a session
690 - * Reschedules if a new user message is received
691 - */
692 -private function schedule_delayed_transcript_email($session_id) {
693 - $options = get_option('mxchat_transcripts_options');
694 -
695 - // Check if auto-email is enabled
696 - if (empty($options['mxchat_auto_email_transcript_enabled'])) {
697 - return;
698 - }
699 -
700 - // Get notification email
701 - $email = !empty($options['mxchat_notification_email']) ?
702 - $options['mxchat_notification_email'] :
703 - get_option('admin_email');
704 -
705 - if (!is_email($email)) {
706 - return;
707 - }
708 -
709 - // Get delay in minutes (default 30)
710 - $delay_minutes = isset($options['mxchat_auto_email_transcript_delay']) ?
711 - intval($options['mxchat_auto_email_transcript_delay']) : 30;
712 -
713 - // Clear any existing scheduled event for this session
714 - $hook = 'mxchat_send_delayed_transcript';
715 - $args = array($session_id);
716 - $timestamp = wp_next_scheduled($hook, $args);
717 -
718 - if ($timestamp) {
719 - wp_unschedule_event($timestamp, $hook, $args);
720 - }
721 -
722 - // Schedule new event
723 - $schedule_time = time() + ($delay_minutes * 60);
724 - wp_schedule_single_event($schedule_time, $hook, $args);
725 -}
726 -
727 -/**
728 - * Check if chat messages contain contact information (email or phone number)
729 - *
730 - * @param array $messages Array of message objects with 'message' property
731 - * @param object|null $session_data Session data object with user_email property
732 - * @return bool True if contact info found, false otherwise
733 - */
734 -private function chat_contains_contact_info($messages, $session_data = null) {
735 - // Check if session already has a stored email
736 - if ($session_data && !empty($session_data->user_email)) {
737 - return true;
738 - }
739 -
740 - // Email regex pattern
741 - $email_pattern = '/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/';
742 -
743 - // Phone number patterns (covers various formats including international, WhatsApp style)
744 - // Matches: +1234567890, (123) 456-7890, 123-456-7890, 123.456.7890, 1234567890, +1 234 567 8900, etc.
745 - $phone_pattern = '/(?:\+?\d{1,3}[-.\s]?)?\(?\d{2,4}\)?[-.\s]?\d{2,4}[-.\s]?\d{2,4}(?:[-.\s]?\d{1,4})?/';
746 -
747 - // Only check user messages (not assistant responses)
748 - foreach ($messages as $msg) {
749 - if ($msg->role !== 'user') {
750 - continue;
751 - }
752 -
753 - $message_text = $msg->message;
754 -
755 - // Check for email
756 - if (preg_match($email_pattern, $message_text)) {
757 - return true;
758 - }
759 -
760 - // Check for phone number (must be at least 7 digits total to avoid false positives)
761 - if (preg_match($phone_pattern, $message_text, $matches)) {
762 - // Count actual digits to avoid matching short numbers
763 - $digits_only = preg_replace('/\D/', '', $matches[0]);
764 - if (strlen($digits_only) >= 7) {
765 - return true;
766 - }
767 - }
768 - }
769 -
770 - return false;
771 -}
772 -
773 -/**
774 - * Send the delayed transcript email with .txt attachment
775 - */
776 -public function mxchat_send_delayed_transcript($session_id) {
777 - global $wpdb;
778 -
779 - $options = get_option('mxchat_transcripts_options');
780 -
781 - // Get notification email
782 - $to = !empty($options['mxchat_notification_email']) ?
783 - $options['mxchat_notification_email'] :
784 - get_option('admin_email');
785 -
786 - if (!is_email($to)) {
787 - return false;
788 - }
789 -
790 - // Get all messages for this session
791 - $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
792 - $messages = $wpdb->get_results($wpdb->prepare(
793 - "SELECT role, message, timestamp FROM {$table_name}
794 - WHERE session_id = %s
795 - ORDER BY timestamp ASC",
796 - $session_id
797 - ));
798 -
799 - if (empty($messages)) {
800 - return false;
801 - }
802 -
803 - // Get session metadata
804 - $sessions_table = $wpdb->prefix . 'mxchat_sessions';
805 - $session_data = $wpdb->get_row($wpdb->prepare(
806 - "SELECT * FROM {$sessions_table} WHERE session_id = %s",
807 - $session_id
808 - ));
809 -
810 - // Check if contact info is required and if it's present
811 - $require_contact = !empty($options['mxchat_auto_email_transcript_require_contact']);
812 - if ($require_contact && !$this->chat_contains_contact_info($messages, $session_data)) {
813 - // Contact info required but not found - skip sending
814 - return false;
815 - }
816 -
817 - // Build transcript content
818 - $transcript_content = "Chat Transcript\n";
819 - $transcript_content .= "================\n\n";
820 - $transcript_content .= "Session ID: " . $session_id . "\n";
821 -
822 - if ($session_data) {
823 - $transcript_content .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
824 - $transcript_content .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
825 - $transcript_content .= "Started: " . $session_data->created_at . "\n";
826 - }
827 -
828 - $transcript_content .= "\n" . str_repeat("=", 50) . "\n\n";
829 -
830 - // Add messages
831 - foreach ($messages as $msg) {
832 - $role_label = ($msg->role === 'user') ? 'User' : 'Assistant';
833 - $transcript_content .= "[{$msg->timestamp}] {$role_label}:\n";
834 - $transcript_content .= $msg->message . "\n\n";
835 - }
836 -
837 - // Create temporary file for attachment using WP_Filesystem
838 - $upload_dir = wp_upload_dir();
839 - $temp_file = $upload_dir['basedir'] . '/mxchat-transcript-' . $session_id . '.txt';
840 - global $wp_filesystem;
841 - if (empty($wp_filesystem)) {
842 - require_once ABSPATH . 'wp-admin/includes/file.php';
843 - WP_Filesystem();
844 - }
845 - $wp_filesystem->put_contents($temp_file, $transcript_content, FS_CHMOD_FILE);
846 -
847 - // Prepare email
848 - $subject = sprintf('[%s] Chat Transcript - Session %s', get_bloginfo('name'), substr($session_id, 0, 8));
849 -
850 - $message = "Please find attached the full chat transcript.\n\n";
851 - $message .= "Session ID: {$session_id}\n";
852 -
853 - if ($session_data) {
854 - $message .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
855 - $message .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
856 - }
857 -
858 - $message .= "\nView online: " . admin_url('admin.php?page=mxchat-transcripts');
859 -
860 - // Send email with attachment
861 - $attachments = array($temp_file);
862 - $result = wp_mail($to, $subject, $message, '', $attachments);
863 -
864 - // Clean up temporary file
865 - if (file_exists($temp_file)) {
866 - unlink($temp_file);
867 - }
868 -
869 - return $result;
870 -}
871 -
872 -
873 -
874 546 public function mxchat_handle_save_email_and_response() {
875 547 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
876 548 //error_log('DEBUG: POST data: ' . print_r($_POST, true));
877 549
878 - nocache_headers();
879 -
880 550 // Validate nonce
881 551 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
882 552 //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
883 553 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
@@ -889,9 +559,9 @@
889 559 $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
890 560
891 561 //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}, name: {$name}");
892 562
893 - if (empty($session_id) || $session_id === 'null' || empty($email)) {
563 + if (empty($session_id) || empty($email)) {
894 564 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
895 565 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
896 566 wp_die();
897 567 }
@@ -908,15 +578,15 @@
908 578 }
909 579
910 580 // 1) Always store email in wp_options
911 581 $email_option_key = "mxchat_email_{$session_id}";
912 - update_option($email_option_key, $email, 'no');
582 + update_option($email_option_key, $email);
913 583 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$email_option_key} => {$email}");
914 584
915 585 // Store name in wp_options if provided
916 586 if (!empty($name)) {
917 587 $name_option_key = "mxchat_name_{$session_id}";
918 - update_option($name_option_key, $name, 'no');
588 + update_option($name_option_key, $name);
919 589 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$name_option_key} => {$name}");
920 590 }
921 591
922 592 // 2) (Optional) Also store in DB if a row already exists
@@ -960,10 +630,8 @@
960 630
961 631 public function mxchat_check_email_provided() {
962 632 //error_log('[DEBUG] ---------- mxchat_check_email_provided START ----------');
963 633
964 - nocache_headers();
965 -
966 634 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
967 635 //error_log('[ERROR] Invalid nonce in mxchat_check_email_provided');
968 636 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
969 637 }
@@ -968,9 +636,9 @@
968 636 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
969 637 }
970 638
971 639 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
972 - if (empty($session_id) || $session_id === 'null') {
640 + if (empty($session_id)) {
973 641 //error_log('[ERROR] No session ID provided in mxchat_check_email_provided');
974 642 wp_send_json_error(['message' => esc_html__('No session ID provided', 'mxchat')]);
975 643 }
976 644
@@ -1027,42 +695,15 @@
1027 695 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
1028 696 }
1029 697 }
1030 698
1031 -/**
1032 - * Send error response in appropriate format based on streaming mode
1033 - * ADDED: Helper method to consistently handle errors in both streaming and non-streaming modes
1034 - *
1035 - * @param string $error_message The error message to display
1036 - * @param string $error_code Optional error code for debugging
1037 - */
1038 -private function send_error_response($error_message, $error_code = 'api_error') {
1039 - if ($this->is_streaming) {
1040 - echo "data: " . json_encode([
1041 - 'error' => true,
1042 - 'error_message' => $error_message,
1043 - 'error_code' => $error_code,
1044 - 'text' => $error_message,
1045 - 'message' => $error_message
1046 - ]) . "\n\n";
1047 - echo "data: [DONE]\n\n";
1048 - flush();
1049 - } else {
1050 - wp_send_json_error([
1051 - 'error_message' => $error_message,
1052 - 'error_code' => $error_code
1053 - ]);
1054 - }
1055 - wp_die();
1056 -}
1057 -
1058 699 public function mxchat_handle_chat_request() {
1059 700 global $wpdb;
1060 701
1061 702 // Debug: Log incoming bot_id
1062 703 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
1063 - //error_log("=== MXCHAT DEBUG: Starting chat request ===");
1064 - //error_log("MXCHAT DEBUG: Bot ID received: " . $bot_id);
704 + error_log("=== MXCHAT DEBUG: Starting chat request ===");
705 + error_log("MXCHAT DEBUG: Bot ID received: " . $bot_id);
1065 706
1066 707 // Get bot-specific options
1067 708 $bot_options = $this->get_bot_options($bot_id);
1068 709 $current_options = !empty($bot_options) ? $bot_options : $this->options;
@@ -1067,19 +708,29 @@
1067 708 $bot_options = $this->get_bot_options($bot_id);
1068 709 $current_options = !empty($bot_options) ? $bot_options : $this->options;
1069 710
1070 711 // Check if this is a streaming request
1071 - // Allow force_streaming_test parameter to bypass the setting check (for admin compatibility testing)
1072 - $force_streaming_test = isset($_POST['force_streaming_test']) && $_POST['force_streaming_test'] === '1' && current_user_can('administrator');
1073 - $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat' &&
1074 - ($force_streaming_test || (isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on'));
712 + $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat' &&
713 + isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on';
714 +
715 + // Set streaming headers if needed
716 + if ($is_streaming) {
717 + // Disable output buffering
718 + while (ob_get_level()) {
719 + ob_end_flush(); // Changed from ob_end_clean()
720 + }
721 +
722 + // Set headers for SSE
723 + header('Content-Type: text/event-stream');
724 + header('Cache-Control: no-cache');
725 + header('Connection: keep-alive');
726 + header('X-Accel-Buffering: no');
727 +
728 + // Add these new lines:
729 + ob_implicit_flush(true);
730 + flush();
731 + }
1075 732
1076 - // ADDED: Store streaming state in class property for use in private methods
1077 - $this->is_streaming = $is_streaming;
1078 -
1079 - // NOTE: Streaming headers are now set later via setup_streaming_headers()
1080 - // This allows actions/forms to return JSON responses without header conflicts
1081 -
1082 733 // Check if MX Chat Moderation is active
1083 734 if (class_exists('MX_Chat_Moderation')) {
1084 735 // Get user email and IP
1085 736 $user_email = '';
@@ -1144,31 +795,13 @@
1144 795
1145 796 // Rest of your existing code...
1146 797 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
1147 798
1148 - // Treat the literal strings 'null' / 'undefined' as missing too. Browser edge cases
1149 - // (Safari ITP, private mode, cross-origin iframes with partitioned storage) can cause
1150 - // the frontend FormData.append() to stringify a null session_id into the literal
1151 - // "null", which would otherwise pass empty() and pollute the transcripts table with
1152 - // ghost sessions that group every visitor's first message under one row.
1153 - if ($session_id === 'null' || $session_id === 'undefined') {
1154 - $session_id = '';
1155 - }
1156 -
1157 799 if (empty($session_id)) {
1158 800 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
1159 801 wp_die();
1160 802 }
1161 803
1162 - // Update session owner if it changed (e.g. IP changed due to network switch)
1163 - // The session ID itself is the authentication — if the client has it, they own it
1164 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
1165 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
1166 -
1167 - if (!$session_owner || $session_owner !== $current_user_identifier) {
1168 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
1169 - }
1170 -
1171 804 // Validate and sanitize the incoming message
1172 805 if (empty($_POST['message'])) {
1173 806 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
1174 807 wp_die();
@@ -1544,8 +1177,38 @@
1544 1177 }
1545 1178 }
1546 1179 }
1547 1180
1181 + // Check if there's an active recommendation flow session
1182 + $flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
1183 + if (!empty($flow_state) && isset($flow_state['flow_id'])) {
1184 + // Create a dummy intent object that matches the original intent
1185 + $dummy_intent = new stdClass();
1186 + $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
1187 + $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
1188 +
1189 + // Call the recommendation flow handler directly
1190 + $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
1191 +
1192 + // If the handler returned a response, send it
1193 + if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
1194 + // Save the bot's response to the chat history
1195 + if (!empty($response_data['text'])) {
1196 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
1197 + }
1198 + if (!empty($response_data['html'])) {
1199 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
1200 + }
1201 +
1202 + if ($testing_data !== null) {
1203 + $response_data['testing_data'] = $testing_data;
1204 + }
1205 +
1206 + // Send the response
1207 + wp_send_json($response_data);
1208 + wp_die();
1209 + }
1210 + }
1548 1211
1549 1212 // Step 2: Detect intent and handle intent-based responses
1550 1213 $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1551 1214
@@ -1564,46 +1227,58 @@
1564 1227 'text' => $intent_result['text'] ?? '',
1565 1228 'html' => $intent_result['html'] ?? '',
1566 1229 'session_id' => $session_id
1567 1230 ];
1568 -
1569 - // IMPORTANT: Include chat_mode if present (for WhatsApp, Slack, etc.)
1570 - if (isset($intent_result['chat_mode'])) {
1571 - $response_data['chat_mode'] = $intent_result['chat_mode'];
1572 - }
1573 -
1231 +
1574 1232 if ($testing_data !== null) {
1575 1233 $response_data['testing_data'] = $testing_data;
1576 1234 }
1577 -
1235 +
1236 + // Clear streaming headers if they were set
1237 + if ($is_streaming) {
1238 + header_remove('Content-Type');
1239 + header_remove('Cache-Control');
1240 + header_remove('Connection');
1241 + header_remove('X-Accel-Buffering');
1242 + header('Content-Type: application/json');
1243 + }
1244 +
1578 1245 wp_send_json($response_data);
1579 1246 wp_die();
1580 1247 } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1581 1248 // Intent returned true and set fallbackResponse
1582 -
1583 - // SAVE TO TRANSCRIPT
1249 +
1250 + // SAVE TO TRANSCRIPT FIRST
1584 1251 if (!empty($this->fallbackResponse['text'])) {
1585 1252 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1586 1253 }
1587 - // Save action HTML (product cards, featured products, etc.) so it renders in transcripts
1588 1254 if (!empty($this->fallbackResponse['html'])) {
1589 1255 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1590 1256 }
1591 -
1257 +
1592 1258 $response_data = [
1593 1259 'text' => $this->fallbackResponse['text'] ?? '',
1594 1260 'html' => $this->fallbackResponse['html'] ?? '',
1595 1261 'session_id' => $session_id
1596 1262 ];
1597 -
1263 +
1598 1264 if (isset($this->fallbackResponse['chat_mode'])) {
1599 1265 $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
1600 1266 }
1601 -
1267 +
1602 1268 if ($testing_data !== null) {
1603 1269 $response_data['testing_data'] = $testing_data;
1604 1270 }
1605 -
1271 +
1272 + // Clear streaming headers if they were set
1273 + if ($is_streaming) {
1274 + header_remove('Content-Type');
1275 + header_remove('Cache-Control');
1276 + header_remove('Connection');
1277 + header_remove('X-Accel-Buffering');
1278 + header('Content-Type: application/json');
1279 + }
1280 +
1606 1281 wp_send_json($response_data);
1607 1282 wp_die();
1608 1283 }
1609 1284 }
@@ -1608,13 +1283,11 @@
1608 1283 }
1609 1284 }
1610 1285
1611 1286 // If we get here, no intent matched OR the intent didn't provide a usable response
1612 -
1287 +
1613 1288 // Step 4: Generate AI response
1614 - // Get session start timestamp - when persistence is OFF, only include messages from this page load
1615 - $session_start_timestamp = isset($_POST['session_start_timestamp']) ? intval($_POST['session_start_timestamp']) : 0;
1616 - $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id, $session_start_timestamp);
1289 + $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
1617 1290 $this->mxchat_increment_chat_count();
1618 1291
1619 1292 // Generate embedding for the user's query - USE BOT-SPECIFIC API KEY
1620 1293 $api_key = $current_options['api_key'] ?? $this->options['api_key'];
@@ -1623,50 +1296,22 @@
1623 1296 // Check if the embedding generation returned an error
1624 1297 if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1625 1298 $error_message = $user_message_embedding['error'];
1626 1299 $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1627 -
1628 - // FIXED: Send error in appropriate format based on streaming mode
1629 - if ($is_streaming) {
1630 - echo "data: " . json_encode([
1631 - 'error' => true,
1632 - 'error_message' => $error_message,
1633 - 'error_code' => $error_code,
1634 - 'text' => $error_message,
1635 - 'message' => $error_message
1636 - ]) . "\n\n";
1637 - echo "data: [DONE]\n\n";
1638 - flush();
1639 - } else {
1640 - wp_send_json_error([
1641 - 'error_message' => $error_message,
1642 - 'error_code' => $error_code
1643 - ]);
1644 - }
1300 +
1301 + wp_send_json_error([
1302 + 'error_message' => $error_message,
1303 + 'error_code' => $error_code
1304 + ]);
1645 1305 wp_die();
1646 1306 }
1647 -
1307 +
1648 1308 // Check if the embedding is valid
1649 1309 if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1650 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
1651 -
1652 - // FIXED: Send error in appropriate format based on streaming mode
1653 - if ($is_streaming) {
1654 - echo "data: " . json_encode([
1655 - 'error' => true,
1656 - 'error_message' => $error_message,
1657 - 'error_code' => 'invalid_embedding',
1658 - 'text' => $error_message,
1659 - 'message' => $error_message
1660 - ]) . "\n\n";
1661 - echo "data: [DONE]\n\n";
1662 - flush();
1663 - } else {
1664 - wp_send_json_error([
1665 - 'error_message' => $error_message,
1666 - 'error_code' => 'invalid_embedding'
1667 - ]);
1668 - }
1310 + wp_send_json_error([
1311 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1312 + 'error_code' => 'invalid_embedding'
1313 + ]);
1669 1314 wp_die();
1670 1315 }
1671 1316
1672 1317 // Build context with both knowledge base and PDF content if available
@@ -1692,71 +1337,26 @@
1692 1337 $context_content .= "Page Content: " . $page_context['content'] . "\n";
1693 1338 $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1694 1339 }
1695 1340
1696 - // Get relevant content from knowledge base - PASS BOT_ID and MESSAGE for Vector Store
1697 - $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id, $message);
1698 -
1699 - // NEW: Also extract URLs from system instructions (only if citation links enabled)
1700 - // Use fresh options to ensure we get the latest setting value
1701 - $fresh_options = get_option('mxchat_options', []);
1702 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
1341 + // Get relevant content from knowledge base - PASS BOT_ID
1342 + $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id);
1703 1343
1704 - $system_instructions = $this->get_system_instructions($bot_id, $session_id);
1705 - if ($citation_links_enabled && !empty($system_instructions)) {
1706 - preg_match_all(
1707 - '#\bhttps?://[^\s<>"\']+#i',
1708 - $system_instructions,
1709 - $system_instruction_urls
1710 - );
1711 -
1712 - if (!empty($system_instruction_urls[0])) {
1713 - // Merge with existing valid URLs
1714 - $this->current_valid_urls = array_merge(
1715 - $this->current_valid_urls,
1716 - $system_instruction_urls[0]
1717 - );
1718 - // Remove duplicates
1719 - $this->current_valid_urls = array_unique($this->current_valid_urls);
1720 -
1721 - //error_log("Added " . count($system_instruction_urls[0]) . " URLs from system instructions");
1722 - }
1344 + // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1345 + if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1346 + // Update testing data with the REAL similarity analysis
1347 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1348 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1349 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1723 1350 }
1351 + // ===== END SIMILARITY DATA CAPTURE =====
1724 1352
1725 -// ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1726 -if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1727 - // Update testing data with the REAL similarity analysis
1728 - $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1729 - $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1730 - $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1731 - $testing_data['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
1732 - $testing_data['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
1733 -}
1734 -// ===== END SIMILARITY DATA CAPTURE =====
1735 -
1736 -// NEW: Add valid URLs to testing data for admin panel display (AFTER similarity data)
1737 -if ($testing_data !== null && !empty($this->current_valid_urls)) {
1738 - $testing_data['approved_urls'] = array_values($this->current_valid_urls);
1739 - //error_log("Added " . count($this->current_valid_urls) . " approved URLs to testing data");
1740 -}
1741 -
1742 1353 if (!empty($relevant_content)) {
1743 1354 $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1744 1355 } else {
1745 1356 $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1746 1357 }
1747 -
1748 - // NEW: Add approved URLs list to context for AI (only if citation links enabled)
1749 - if ($citation_links_enabled && !empty($this->current_valid_urls)) {
1750 - $context_content .= "===== APPROVED URLS FOR CITATIONS =====\n";
1751 - $context_content .= "You may ONLY use these exact URLs in your response:\n";
1752 - foreach ($this->current_valid_urls as $url) {
1753 - $context_content .= "- " . $url . "\n";
1754 - }
1755 - $context_content .= "\nCRITICAL: Do NOT create, modify, extend, or invent any other URLs. ";
1756 - $context_content .= "===== END APPROVED URLS =====\n\n";
1757 - }
1758 -
1358 +
1759 1359 // Check for and include PDF content
1760 1360 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1761 1361 $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1762 1362 $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
@@ -1788,9 +1388,9 @@
1788 1388
1789 1389 $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1790 1390
1791 1391 // Extract model from current options for bot-specific model support
1792 - $selected_model = isset($current_options['model']) ? $current_options['model'] : 'gpt-5.1-chat-latest';
1392 + $selected_model = isset($current_options['model']) ? $current_options['model'] : 'gpt-4o';
1793 1393
1794 1394 $response = $this->mxchat_generate_response(
1795 1395 $context_content,
1796 1396 $current_options['api_key'] ?? $this->options['api_key'],
@@ -1797,14 +1397,13 @@
1797 1397 $current_options['xai_api_key'] ?? $this->options['xai_api_key'],
1798 1398 $current_options['claude_api_key'] ?? $this->options['claude_api_key'],
1799 1399 $current_options['deepseek_api_key'] ?? $this->options['deepseek_api_key'],
1800 1400 $current_options['gemini_api_key'] ?? $this->options['gemini_api_key'],
1801 - $current_options['openrouter_api_key'] ?? $this->options['openrouter_api_key'],
1802 1401 $conversation_history,
1803 1402 $is_streaming,
1804 1403 $session_id,
1805 1404 $testing_data,
1806 - $selected_model
1405 + $selected_model // ADD THIS LINE
1807 1406 );
1808 1407
1809 1408 // Handle streaming vs non-streaming responses
1810 1409 if ($is_streaming) {
@@ -1812,27 +1411,11 @@
1812 1411 if ($response === true) {
1813 1412 wp_die();
1814 1413 }
1815 1414 // If we get here, streaming fell back to regular response, continue
1816 - // But if there's an error, we need to send it as SSE format since headers are already set
1817 - if (is_array($response) && isset($response['error'])) {
1818 - $error_message = $response['error'];
1819 - $error_code = $response['error_code'] ?? 'api_error';
1820 - // Send error in SSE format that the client JS can handle
1821 - echo "data: " . json_encode([
1822 - 'error' => true,
1823 - 'error_message' => $error_message,
1824 - 'error_code' => $error_code,
1825 - 'text' => $error_message, // Also include as text for fallback handling
1826 - 'message' => $error_message
1827 - ]) . "\n\n";
1828 - echo "data: [DONE]\n\n";
1829 - flush();
1830 - wp_die();
1831 - }
1832 1415 }
1833 -
1834 - // Check if the response is an error array (non-streaming mode)
1416 +
1417 + // Check if the response is an error array
1835 1418 if (is_array($response) && isset($response['error'])) {
1836 1419 wp_send_json_error([
1837 1420 'error_message' => $response['error'],
1838 1421 'error_code' => $response['error_code'] ?? 'api_error'
@@ -1839,51 +1422,11 @@
1839 1422 ]);
1840 1423 wp_die();
1841 1424 }
1842 1425
1843 - // DEBUG: Check what we have
1844 - //error_log("=== BEFORE URL VALIDATION ===");
1845 - //error_log("current_valid_urls is empty? " . (empty($this->current_valid_urls) ? 'YES' : 'NO'));
1846 - //error_log("current_valid_urls count: " . count($this->current_valid_urls));
1847 - //error_log("current_valid_urls content: " . print_r($this->current_valid_urls, true));
1848 -
1849 - // If we get here, the response is valid text - now validate URLs
1850 - if (!empty($this->current_valid_urls)) {
1851 - //error_log("CALLING validate_and_clean_urls");
1852 - $response = $this->validate_and_clean_urls($response, $this->current_valid_urls);
1853 - } else {
1854 - //error_log("SKIPPING validation - current_valid_urls is empty");
1855 - }
1856 - // ===== END URL VALIDATION =====
1426 + // If we get here, the response is valid text
1427 + $this->mxchat_save_chat_message($session_id, 'bot', $response);
1857 1428
1858 - // Prepare RAG context data for storage (only include documents used for context)
1859 - $rag_context_for_storage = null;
1860 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
1861 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
1862 -
1863 - if ($has_rag_data || $has_action_data) {
1864 - $rag_context_for_storage = [];
1865 -
1866 - // Add RAG/source data if available
1867 - if ($has_rag_data) {
1868 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
1869 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
1870 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
1871 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
1872 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1873 - $rag_context_for_storage['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
1874 - $rag_context_for_storage['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
1875 - }
1876 -
1877 - // Add action analysis data if available
1878 - if ($has_action_data) {
1879 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
1880 - }
1881 - }
1882 -
1883 - // Save the cleaned response with RAG context
1884 - $this->mxchat_save_chat_message($session_id, 'bot', $response, null, $rag_context_for_storage);
1885 -
1886 1429 // Step 5: Save additional content if available
1887 1430 if (!empty($this->productCardHtml)) {
1888 1431 $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1889 1432 }
@@ -1892,13 +1435,8 @@
1892 1435 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1893 1436 }
1894 1437
1895 1438 // Step 6: Return the response
1896 - // DEBUG: Check if newlines exist in the response
1897 - //error_log("=== MXCHAT NON-STREAMING RESPONSE DEBUG ===");
1898 - //error_log("Response has newlines: " . (strpos($response, "\n") !== false ? 'YES' : 'NO'));
1899 - //error_log("Response first 500 chars: " . substr($response, 0, 500));
1900 -
1901 1439 $response_data = [
1902 1440 'text' => $response,
1903 1441 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1904 1442 'session_id' => $session_id
@@ -1903,18 +1441,8 @@
1903 1441 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1904 1442 'session_id' => $session_id
1905 1443 ];
1906 1444
1907 - // Include vectorstore error info for admin debugging (only visible to admins via testing_data)
1908 - if (!empty($this->last_vectorstore_error) && $testing_data !== null) {
1909 - $testing_data['vectorstore_error'] = $this->last_vectorstore_error;
1910 - }
1911 -
1912 - // Also pass it as a top-level field so JS can show a better error message to admins
1913 - if (!empty($this->last_vectorstore_error) && current_user_can('manage_options')) {
1914 - $response_data['vectorstore_error'] = $this->last_vectorstore_error;
1915 - }
1916 -
1917 1445 // Always add testing data for admins (no toggle needed)
1918 1446 if ($testing_data !== null) {
1919 1447 $response_data['testing_data'] = $testing_data;
1920 1448 }
@@ -1922,8 +1450,9 @@
1922 1450 wp_send_json($response_data);
1923 1451 wp_die();
1924 1452 }
1925 1453
1454 +
1926 1455 /**
1927 1456 * Get bot-specific options for multi-bot functionality
1928 1457 * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1929 1458 */
@@ -1928,12 +1457,12 @@
1928 1457 * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1929 1458 */
1930 1459 // Also debug the bot options retrieval
1931 1460 private function get_bot_options($bot_id = 'default') {
1932 - //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
1461 + error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
1933 1462
1934 1463 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1935 - //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
1464 + error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
1936 1465 return array();
1937 1466 }
1938 1467
1939 1468 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
@@ -1938,11 +1467,11 @@
1938 1467
1939 1468 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
1940 1469
1941 1470 if (!empty($bot_options)) {
1942 - //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1471 + error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1943 1472 if (isset($bot_options['similarity_threshold'])) {
1944 - //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1473 + error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1945 1474 }
1946 1475 }
1947 1476
1948 1477 return is_array($bot_options) ? $bot_options : array();
@@ -1953,13 +1482,13 @@
1953 1482 * Used in the knowledge retrieval functions
1954 1483 */
1955 1484 // Also add debugging to your get_bot_pinecone_config function
1956 1485 private function get_bot_pinecone_config($bot_id = 'default') {
1957 - //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1486 + error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1958 1487
1959 1488 // If default bot or multi-bot add-on not active, use default Pinecone config
1960 1489 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1961 - //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1490 + error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1962 1491 $addon_options = get_option('mxchat_pinecone_addon_options', array());
1963 1492 $config = array(
1964 1493 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
1965 1494 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
@@ -1965,24 +1494,24 @@
1965 1494 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
1966 1495 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
1967 1496 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
1968 1497 );
1969 - //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1498 + error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1970 1499 return $config;
1971 1500 }
1972 1501
1973 - //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1502 + error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1974 1503
1975 1504 // Hook for multi-bot add-on to provide bot-specific Pinecone config
1976 1505 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1977 1506
1978 1507 if (!empty($bot_pinecone_config)) {
1979 - //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1980 - //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1981 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1982 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1508 + error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1509 + error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1510 + error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1511 + error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1983 1512 } else {
1984 - //error_log("MXCHAT DEBUG: Filter returned empty config!");
1513 + error_log("MXCHAT DEBUG: Filter returned empty config!");
1985 1514 }
1986 1515
1987 1516 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1988 1517 }
@@ -1992,63 +1521,35 @@
1992 1521 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
1993 1522 global $wpdb;
1994 1523 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1995 1524
1996 - // Get the current bot_id
1525 + // NEW: Get the current bot_id
1997 1526 $current_bot_id = $this->get_current_bot_id($session_id);
1998 1527
1999 1528 // Generate the user embedding
2000 1529 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
2001 -
1530 +
2002 1531 // Check if embedding generation returned an error
2003 1532 if (is_array($user_embedding) && isset($user_embedding['error'])) {
2004 1533 $error_message = $user_embedding['error'];
2005 1534 $error_code = $user_embedding['error_code'] ?? 'embedding_error';
2006 -
2007 - // FIXED: Send error in appropriate format based on streaming mode
2008 - if ($this->is_streaming) {
2009 - echo "data: " . json_encode([
2010 - 'error' => true,
2011 - 'error_message' => $error_message,
2012 - 'error_code' => $error_code,
2013 - 'text' => $error_message,
2014 - 'message' => $error_message
2015 - ]) . "\n\n";
2016 - echo "data: [DONE]\n\n";
2017 - flush();
2018 - } else {
2019 - wp_send_json_error([
2020 - 'error_message' => $error_message,
2021 - 'error_code' => $error_code
2022 - ]);
2023 - }
1535 +
1536 + wp_send_json_error([
1537 + 'error_message' => $error_message,
1538 + 'error_code' => $error_code
1539 + ]);
2024 1540 wp_die();
2025 1541 }
2026 -
1542 +
2027 1543 // Check if embedding is valid
2028 1544 if (!is_array($user_embedding) || empty($user_embedding)) {
2029 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
2030 -
2031 - // FIXED: Send error in appropriate format based on streaming mode
2032 - if ($this->is_streaming) {
2033 - echo "data: " . json_encode([
2034 - 'error' => true,
2035 - 'error_message' => $error_message,
2036 - 'error_code' => 'invalid_embedding',
2037 - 'text' => $error_message,
2038 - 'message' => $error_message
2039 - ]) . "\n\n";
2040 - echo "data: [DONE]\n\n";
2041 - flush();
2042 - } else {
2043 - wp_send_json_error([
2044 - 'error_message' => $error_message,
2045 - 'error_code' => 'invalid_embedding'
2046 - ]);
2047 - }
1545 + wp_send_json_error([
1546 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1547 + 'error_code' => 'invalid_embedding'
1548 + ]);
2048 1549 wp_die();
2049 1550 }
2050 -
1551 +
2051 1552 // Fetch intents from the database
2052 1553 $table_name = $wpdb->prefix . 'mxchat_intents';
2053 1554 if ($chat_mode === 'agent') {
2054 1555 $query = $wpdb->prepare(
@@ -2058,29 +1559,19 @@
2058 1559 $intents = $wpdb->get_results($query);
2059 1560 } else {
2060 1561 $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
2061 1562 }
2062 -
1563 +
2063 1564 if (empty($intents)) {
2064 1565 return false;
2065 1566 }
2066 -
2067 - // Prefetch individual phrase vectors from wp_mxchat_intent_phrases (grouped by intent_id)
2068 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
2069 - $phrases_by_intent = [];
2070 - if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
2071 - $all_phrases = $wpdb->get_results("SELECT intent_id, phrase, embedding_vector FROM $phrases_table");
2072 - foreach ($all_phrases as $p) {
2073 - $phrases_by_intent[$p->intent_id][] = $p;
2074 - }
2075 - }
2076 -
1567 +
2077 1568 $highest_similarity = -INF;
2078 1569 $matched_intent = null;
2079 -
1570 +
2080 1571 // Array to store action analysis for testing panel
2081 1572 $action_analysis = [];
2082 -
1573 +
2083 1574 foreach ($intents as $intent) {
2084 1575 // Additional check for enabled state
2085 1576 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2086 1577 if (!$is_enabled) {
@@ -2085,56 +1576,26 @@
2085 1576 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2086 1577 if (!$is_enabled) {
2087 1578 continue;
2088 1579 }
2089 -
2090 - // Check if this action is enabled for the current bot
1580 +
1581 + // NEW: Check if this action is enabled for the current bot
2091 1582 if (!$this->is_action_enabled_for_bot($intent, $current_bot_id)) {
2092 1583 continue;
2093 1584 }
2094 -
2095 - $best_similarity = -INF;
2096 - $matched_phrase_text = '';
2097 -
2098 - // Check legacy embedding vector (existing behavior)
1585 +
2099 1586 $intent_embedding_serialized = $intent->embedding_vector;
2100 1587 $intent_embedding = $intent_embedding_serialized
2101 1588 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
2102 1589 : null;
2103 -
2104 - if (is_array($intent_embedding) && !empty($intent_embedding)) {
2105 - $legacy_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2106 - if ($legacy_similarity > $best_similarity) {
2107 - $best_similarity = $legacy_similarity;
2108 - $matched_phrase_text = 'legacy';
2109 - }
2110 - }
2111 -
2112 - // Check individual phrase vectors
2113 - if (isset($phrases_by_intent[$intent->id])) {
2114 - foreach ($phrases_by_intent[$intent->id] as $phrase_row) {
2115 - $phrase_embedding = $phrase_row->embedding_vector
2116 - ? unserialize($phrase_row->embedding_vector, ['allowed_classes' => false])
2117 - : null;
2118 - if (!is_array($phrase_embedding)) {
2119 - continue;
2120 - }
2121 - $phrase_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $phrase_embedding);
2122 - if ($phrase_similarity > $best_similarity) {
2123 - $best_similarity = $phrase_similarity;
2124 - $matched_phrase_text = $phrase_row->phrase;
2125 - }
2126 - }
2127 - }
2128 -
2129 - // Skip if no valid embedding was found at all
2130 - if ($best_similarity === -INF) {
1590 +
1591 + if (!is_array($intent_embedding)) {
2131 1592 continue;
2132 1593 }
2133 -
2134 - $similarity = $best_similarity;
1594 +
1595 + $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2135 1596 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
2136 -
1597 +
2137 1598 // Store action analysis data for testing panel
2138 1599 $action_analysis[] = [
2139 1600 'intent_label' => $intent->intent_label,
2140 1601 'callback_function' => $intent->callback_function,
@@ -2142,12 +1603,11 @@
2142 1603 'similarity_percentage' => round($similarity * 100, 2),
2143 1604 'threshold' => $intent_threshold,
2144 1605 'threshold_percentage' => round($intent_threshold * 100, 2),
2145 1606 'above_threshold' => $similarity >= $intent_threshold,
2146 - 'matched_phrase' => $matched_phrase_text,
2147 1607 'triggered' => false // Will be updated below if this intent is triggered
2148 1608 ];
2149 -
1609 +
2150 1610 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
2151 1611 $highest_similarity = $similarity;
2152 1612 $matched_intent = $intent;
2153 1613 }
@@ -2218,22 +1678,16 @@
2218 1678 // If enabled_bots column doesn't exist or is null, assume it's enabled for all bots (backward compatibility)
2219 1679 if (!isset($intent->enabled_bots) || empty($intent->enabled_bots)) {
2220 1680 return true;
2221 1681 }
2222 -
1682 +
2223 1683 $enabled_bots = json_decode($intent->enabled_bots, true);
2224 -
1684 +
2225 1685 // If JSON decode fails or returns empty array, assume enabled for all (backward compatibility)
2226 1686 if (!is_array($enabled_bots) || empty($enabled_bots)) {
2227 1687 return true;
2228 1688 }
2229 -
2230 - // Admin testing tab uses bot_id "testing" — treat it as "default" so all
2231 - // default-bot actions are testable from the admin panel
2232 - if ($bot_id === 'testing') {
2233 - $bot_id = 'default';
2234 - }
2235 -
1689 +
2236 1690 // Check if the current bot is in the enabled bots list
2237 1691 return in_array($bot_id, $enabled_bots);
2238 1692 }
2239 1693
@@ -2271,23 +1725,18 @@
2271 1725 }
2272 1726
2273 1727 public function mxchat_generate_image($message, $user_id, $session_id) {
2274 1728 //error_log("Starting image generation for message: " . $message);
2275 -
2276 - // Prepare a prompt for OpenAI image generation
1729 +
1730 + // Prepare a prompt for DALL-E
2277 1731 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2278 -
2279 - // Opt-in routing: when 'custom_provider_for_images' is on, route image gen
2280 - // through the configured Custom (OpenAI-compatible) /images/generations route.
2281 - if (!empty($this->options['custom_provider_for_images']) && $this->options['custom_provider_for_images'] === 'on') {
2282 - $image_response = $this->mxchat_generate_custom_image($prompt);
2283 - } else {
2284 - // Use the existing OpenAI API key
2285 - $openai_api_key = sanitize_text_field($this->options['api_key']);
2286 - // Call OpenAI GPT Image to generate an image
2287 - $image_response = $this->mxchat_generate_openai_image($prompt, $openai_api_key);
2288 - }
2289 1732
1733 + // Use the existing OpenAI API key
1734 + $openai_api_key = sanitize_text_field($this->options['api_key']);
1735 +
1736 + // Call DALL-E to generate an image
1737 + $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1738 +
2290 1739 // Check if the response contains an image URL
2291 1740 if (isset($image_response['imageUrl'])) {
2292 1741 $image_url = esc_url_raw($image_response['imageUrl']);
2293 1742
@@ -2330,103 +1779,24 @@
2330 1779 // Return the response directly instead of relying on the property
2331 1780 return $this->fallbackResponse;
2332 1781 }
2333 1782 }
2334 -
2335 -public function mxchat_generate_gemini_image($message, $user_id, $session_id) {
2336 - $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2337 -
2338 - $gemini_api_key = sanitize_text_field($this->options['gemini_api_key'] ?? '');
2339 - if (empty($gemini_api_key)) {
2340 - $response_text = esc_html__("Gemini API key is not configured.", 'mxchat');
2341 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2342 - return ['text' => $response_text, 'html' => '', 'images' => []];
2343 - }
2344 -
2345 - $image_response = $this->mxchat_generate_imagen_image($prompt, $gemini_api_key);
2346 -
2347 - if (isset($image_response['imageUrl'])) {
2348 - $image_url = esc_url_raw($image_response['imageUrl']);
2349 -
2350 - $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
2351 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
2352 -
2353 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2354 - $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
2355 -
2356 - $this->fallbackResponse = [
2357 - 'text' => $response_text,
2358 - 'html' => $response_html,
2359 - 'images' => [$image_url]
2360 - ];
2361 -
2362 - return $this->fallbackResponse;
2363 - } else {
2364 - $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
2365 -
2366 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2367 -
2368 - $this->fallbackResponse = [
2369 - 'text' => $response_text,
2370 - 'html' => '',
2371 - 'images' => []
2372 - ];
2373 -
2374 - return $this->fallbackResponse;
2375 - }
2376 -}
2377 -
2378 -private function mxchat_save_generated_image($base64_data, $mime_type = 'image/png', $prefix = 'mxchat-generated') {
2379 - $extension = ($mime_type === 'image/jpeg') ? 'jpg' : 'png';
2380 - $filename = sanitize_file_name($prefix . '-' . wp_generate_uuid4() . '.' . $extension);
2381 - $decoded = base64_decode($base64_data);
2382 -
2383 - if ($decoded === false) {
2384 - return new \WP_Error('decode_failed', esc_html__('Failed to decode image data.', 'mxchat'));
2385 - }
2386 -
2387 - $upload = wp_upload_bits($filename, null, $decoded);
2388 -
2389 - if (!empty($upload['error'])) {
2390 - return new \WP_Error('upload_failed', $upload['error']);
2391 - }
2392 -
2393 - $attach_id = wp_insert_attachment([
2394 - 'post_mime_type' => $mime_type,
2395 - 'post_title' => $prefix,
2396 - 'post_content' => '',
2397 - 'post_status' => 'inherit',
2398 - ], $upload['file']);
2399 -
2400 - if (is_wp_error($attach_id)) {
2401 - return $attach_id;
2402 - }
2403 -
2404 - require_once ABSPATH . 'wp-admin/includes/image.php';
2405 - $metadata = wp_generate_attachment_metadata($attach_id, $upload['file']);
2406 - wp_update_attachment_metadata($attach_id, $metadata);
2407 -
2408 - return esc_url_raw(wp_get_attachment_url($attach_id));
2409 -}
2410 -
2411 -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) {
2412 1784 $api_url = 'https://api.openai.com/v1/images/generations';
2413 1785 $body = json_encode([
2414 - 'prompt' => sanitize_text_field($prompt),
2415 - 'n' => 1,
2416 - 'size' => '1024x1024',
2417 - 'quality' => 'medium',
2418 - 'output_format' => 'png',
2419 - 'model' => sanitize_text_field($model),
1786 + 'prompt' => sanitize_text_field($prompt),
1787 + 'n' => 1,
1788 + 'size' => '1024x1024',
1789 + 'model' => sanitize_text_field($model),
2420 1790 ]);
2421 1791
2422 1792 $args = [
2423 - 'body' => $body,
1793 + 'body' => $body,
2424 1794 'headers' => [
2425 - 'Content-Type' => 'application/json',
1795 + 'Content-Type' => 'application/json',
2426 1796 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
2427 1797 ],
2428 - 'method' => 'POST',
1798 + 'method' => 'POST',
2429 1799 'timeout' => absint($timeout),
2430 1800 ];
2431 1801
2432 1802 $response = wp_remote_post($api_url, $args);
@@ -2431,114 +1801,23 @@
2431 1801
2432 1802 $response = wp_remote_post($api_url, $args);
2433 1803
2434 1804 if (is_wp_error($response)) {
1805 + //error_log("DALL-E request failed: " . $response->get_error_message());
2435 1806 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2436 1807 }
2437 1808
2438 1809 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2439 1810
2440 - $b64 = $response_body['data'][0]['b64_json'] ?? $response_body['data'][0]['b64'] ?? null;
2441 - if ($b64) {
2442 - $saved_url = $this->mxchat_save_generated_image($b64, 'image/png', 'mxchat-openai');
2443 - if (is_wp_error($saved_url)) {
2444 - return ['error' => $saved_url->get_error_message()];
2445 - }
2446 - return ['imageUrl' => $saved_url];
1811 + if (isset($response_body['data'][0]['url'])) {
1812 + return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
2447 1813 } else {
1814 + //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
2448 1815 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2449 1816 }
2450 1817 }
2451 1818
2452 1819 /**
2453 - * Generate an image via a Custom (OpenAI-compatible) provider's /images/generations route.
2454 - * Only called when the opt-in 'custom_provider_for_images' setting is on.
2455 - */
2456 -private function mxchat_generate_custom_image($prompt, $timeout = 90) {
2457 - $cfg = $this->mxchat_resolve_custom_provider();
2458 - if (empty($cfg['base_url'])) {
2459 - return ['error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat')];
2460 - }
2461 - $url = $cfg['base_url'] . '/images/generations';
2462 - if (!empty($cfg['api_version'])) {
2463 - $url .= (strpos($url, '?') === false ? '?' : '&') . 'api-version=' . rawurlencode($cfg['api_version']);
2464 - }
2465 - $body = wp_json_encode([
2466 - 'prompt' => sanitize_text_field($prompt),
2467 - 'n' => 1,
2468 - 'size' => '1024x1024',
2469 - 'model' => $cfg['model'],
2470 - ]);
2471 - $response = wp_remote_post($url, [
2472 - 'headers' => $this->mxchat_custom_provider_assoc_headers($cfg),
2473 - 'body' => $body,
2474 - 'method' => 'POST',
2475 - 'timeout' => absint($timeout),
2476 - ]);
2477 - if (is_wp_error($response)) {
2478 - return ['error' => esc_html__('Error generating image (custom provider): ', 'mxchat') . $response->get_error_message()];
2479 - }
2480 - $resp = json_decode(wp_remote_retrieve_body($response), true);
2481 - // Try b64 first (matches OpenAI shape), then url-based fallback.
2482 - $b64 = $resp['data'][0]['b64_json'] ?? $resp['data'][0]['b64'] ?? null;
2483 - if ($b64) {
2484 - $saved = $this->mxchat_save_generated_image($b64, 'image/png', 'mxchat-custom');
2485 - if (is_wp_error($saved)) {
2486 - return ['error' => $saved->get_error_message()];
2487 - }
2488 - return ['imageUrl' => $saved];
2489 - }
2490 - $remote_url = $resp['data'][0]['url'] ?? null;
2491 - if ($remote_url) {
2492 - return ['imageUrl' => esc_url_raw($remote_url)];
2493 - }
2494 - $err_msg = $resp['error']['message'] ?? esc_html__('Custom provider did not return an image.', 'mxchat');
2495 - return ['error' => esc_html($err_msg)];
2496 -}
2497 -
2498 -private function mxchat_generate_imagen_image($prompt, $api_key, $timeout = 60) {
2499 - $api_url = 'https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict';
2500 -
2501 - $body = json_encode([
2502 - 'instances' => [['prompt' => sanitize_text_field($prompt)]],
2503 - 'parameters' => [
2504 - 'sampleCount' => 1,
2505 - 'aspectRatio' => '1:1',
2506 - ],
2507 - ]);
2508 -
2509 - $args = [
2510 - 'body' => $body,
2511 - 'headers' => [
2512 - 'Content-Type' => 'application/json',
2513 - 'x-goog-api-key' => sanitize_text_field($api_key),
2514 - ],
2515 - 'method' => 'POST',
2516 - 'timeout' => absint($timeout),
2517 - ];
2518 -
2519 - $response = wp_remote_post($api_url, $args);
2520 -
2521 - if (is_wp_error($response)) {
2522 - return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2523 - }
2524 -
2525 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
2526 -
2527 - $b64 = $response_body['predictions'][0]['bytesBase64Encoded'] ?? $response_body['predictions'][0]['imageBytes'] ?? null;
2528 - if ($b64) {
2529 - $mime = $response_body['predictions'][0]['mimeType'] ?? 'image/png';
2530 - $saved_url = $this->mxchat_save_generated_image($b64, $mime, 'mxchat-gemini');
2531 - if (is_wp_error($saved_url)) {
2532 - return ['error' => $saved_url->get_error_message()];
2533 - }
2534 - return ['imageUrl' => $saved_url];
2535 - } else {
2536 - return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2537 - }
2538 -}
2539 -
2540 -/**
2541 1820 * Handle web search requests.
2542 1821 *
2543 1822 * Sends the refined search query to the Brave Search API and uses the
2544 1823 * results to generate a conversational response with the AI model.
@@ -2586,10 +1865,10 @@
2586 1865 $transient_key = 'mxchat_search_' . md5($refined_search_query);
2587 1866 $results = get_transient($transient_key);
2588 1867
2589 1868 if (false === $results) {
2590 - // SECURITY FIX: Changed to wp_safe_remote_get
2591 - $response = wp_safe_remote_get(
1869 + // Fetch new results from the Brave Search API
1870 + $response = wp_remote_get(
2592 1871 $api_url,
2593 1872 array(
2594 1873 'headers' => array(
2595 1874 'Accept' => 'application/json',
@@ -2728,10 +2007,9 @@
2728 2007 ],
2729 2008 'timeout' => 10,
2730 2009 ];
2731 2010
2732 - // SECURITY FIX: Changed to wp_safe_remote_get
2733 - $response = wp_safe_remote_get($api_url, $args);
2011 + $response = wp_remote_get($api_url, $args);
2734 2012
2735 2013 if (is_wp_error($response)) {
2736 2014 return array(
2737 2015 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
@@ -2801,22 +2079,17 @@
2801 2079 * @return string The refined search query
2802 2080 */
2803 2081 public function mxchat_interpret_search_query($user_query) {
2804 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');
2805 -
2083 +
2806 2084 // Get options and determine the selected model
2807 2085 $options = $this->options ?? get_option('mxchat_options');
2808 - $selected_model = isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest';
2809 -
2810 - // Custom (OpenAI-compatible) provider routes by model id, not prefix.
2811 - if ($selected_model === 'custom-provider') {
2812 - return $this->interpret_query_with_custom($user_query, $system_prompt);
2813 - }
2814 -
2086 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
2087 +
2815 2088 // Extract model prefix to determine the provider
2816 2089 $model_parts = explode('-', $selected_model);
2817 2090 $provider = strtolower($model_parts[0]);
2818 -
2091 +
2819 2092 // Determine which API key to use based on the provider
2820 2093 switch ($provider) {
2821 2094 case 'gemini':
2822 2095 $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
@@ -2857,60 +2130,11 @@
2857 2130 }
2858 2131 }
2859 2132
2860 2133 /**
2861 - * Interpret query against the configured Custom (OpenAI-compatible) provider.
2862 - * Uses the same base URL + auth scheme as the chat dispatcher.
2863 - */
2864 -private function interpret_query_with_custom($user_query, $system_prompt) {
2865 - $cfg = $this->mxchat_resolve_custom_provider();
2866 - if (empty($cfg['base_url'])) {
2867 - return sanitize_text_field($user_query);
2868 - }
2869 - $args = [
2870 - 'headers' => $this->mxchat_custom_provider_assoc_headers($cfg),
2871 - 'body' => wp_json_encode([
2872 - 'model' => $cfg['model'],
2873 - 'messages' => [
2874 - ['role' => 'system', 'content' => $system_prompt],
2875 - ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2876 - ],
2877 - 'temperature' => 0.2,
2878 - 'max_tokens' => 20,
2879 - ]),
2880 - 'method' => 'POST',
2881 - 'timeout' => 15,
2882 - ];
2883 - $response = wp_remote_post($cfg['chat_url'], $args);
2884 - if (is_wp_error($response)) {
2885 - return sanitize_text_field($user_query);
2886 - }
2887 - $body = json_decode(wp_remote_retrieve_body($response), true);
2888 - return isset($body['choices'][0]['message']['content'])
2889 - ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
2890 - : sanitize_text_field($user_query);
2891 -}
2892 -
2893 -/**
2894 - * Convert the colon-style header list returned by mxchat_resolve_custom_provider
2895 - * into the assoc-array form wp_remote_post expects.
2896 - */
2897 -private function mxchat_custom_provider_assoc_headers($cfg) {
2898 - $headers = ['Content-Type' => 'application/json'];
2899 - if (!empty($cfg['api_key'])) {
2900 - if (($cfg['auth_scheme'] ?? 'bearer') === 'api-key') {
2901 - $headers['api-key'] = $cfg['api_key'];
2902 - } else {
2903 - $headers['Authorization'] = 'Bearer ' . $cfg['api_key'];
2904 - }
2905 - }
2906 - return $headers;
2907 -}
2908 -
2909 -/**
2910 2134 * Interpret query using OpenAI models
2911 2135 */
2912 -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') {
2913 2137 $url = 'https://api.openai.com/v1/chat/completions';
2914 2138 $args = [
2915 2139 'headers' => [
2916 2140 'Authorization' => 'Bearer ' . $api_key,
@@ -2981,13 +2205,13 @@
2981 2205 /**
2982 2206 * Interpret query using Gemini models
2983 2207 */
2984 2208 private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
2985 - // Use v1beta for preview models, v1 for stable models
2986 - $api_version = (strpos($model, 'preview') !== false || strpos($model, 'exp') !== false) ? 'v1beta' : 'v1';
2987 -
2988 - $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);
2989 2211
2212 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
2213 +
2990 2214 $args = [
2991 2215 'headers' => [
2992 2216 'Content-Type' => 'application/json',
2993 2217 ],
@@ -3183,9 +2407,9 @@
3183 2407 }
3184 2408
3185 2409
3186 2410 /**
3187 - * Enhanced fetch_and_split_pdf_pages with SSRF protection
2411 + * Enhanced fetch_and_split_pdf_pages with detailed debugging
3188 2412 */
3189 2413 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
3190 2414 // CLEAR DEBUG LOGGING
3191 2415 //error_log("=== MXCHAT PDF PROCESSING START ===");
@@ -3240,19 +2464,10 @@
3240 2464 // (I'll include the key parts with debug logging)
3241 2465
3242 2466 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
3243 2467 //error_log("Downloading PDF from URL...");
3244 -
3245 - // SECURITY FIX: Validate URL before processing
3246 - if (!$this->mxchat_is_safe_pdf_url($pdf_source)) {
3247 - //error_log("❌ SECURITY: Blocked unsafe PDF URL");
3248 - return false;
3249 - }
3250 -
3251 2468 $temp_file = wp_tempnam($pdf_source);
3252 -
3253 - // SECURITY FIX: Changed from wp_remote_get to wp_safe_remote_get
3254 - $response = wp_safe_remote_get($pdf_source, [
2469 + $response = wp_remote_get($pdf_source, [
3255 2470 'timeout' => 60,
3256 2471 'headers' => ['User-Agent' => 'MxChat PDF Processor']
3257 2472 ]);
3258 2473
@@ -3261,14 +2476,9 @@
3261 2476 //error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
3262 2477 return false;
3263 2478 }
3264 2479
3265 - global $wp_filesystem;
3266 - if (empty($wp_filesystem)) {
3267 - require_once ABSPATH . 'wp-admin/includes/file.php';
3268 - WP_Filesystem();
3269 - }
3270 - $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));
3271 2481 //error_log("✅ PDF downloaded successfully");
3272 2482 } else {
3273 2483 $temp_file = $pdf_source;
3274 2484 //error_log("Using local PDF file: " . $temp_file);
@@ -3275,9 +2485,8 @@
3275 2485 }
3276 2486
3277 2487 // Parse PDF
3278 2488 //error_log("Parsing PDF with basic parser...");
3279 - mxchat_load_pdf_parser();
3280 2489 $parser = new \Smalot\PdfParser\Parser();
3281 2490 $pdf = $parser->parseFile($temp_file);
3282 2491 $pages = $pdf->getPages();
3283 2492
@@ -3340,33 +2549,8 @@
3340 2549 return false;
3341 2550 }
3342 2551 }
3343 2552
3344 -
3345 -/**
3346 - * Validate PDF URL for security
3347 - * Prevents SSRF attacks by blocking dangerous URLs
3348 - */
3349 -
3350 -private function mxchat_is_safe_pdf_url($url) {
3351 - // Use WordPress core function for comprehensive validation
3352 - // This blocks localhost, private IPs, and reserved IP ranges
3353 - $validated_url = wp_http_validate_url($url);
3354 -
3355 - if ($validated_url === false) {
3356 - return false;
3357 - }
3358 -
3359 - // Additional check: only allow HTTP/HTTPS schemes
3360 - $parsed = parse_url($url);
3361 - if (!isset($parsed['scheme']) || !in_array($parsed['scheme'], ['http', 'https'], true)) {
3362 - return false;
3363 - }
3364 -
3365 - return true;
3366 -}
3367 -
3368 -
3369 2553 private function mxchat_clean_text($text) {
3370 2554 // Remove excessive whitespace
3371 2555 $text = preg_replace('/\s+/', ' ', $text);
3372 2556
@@ -3405,10 +2589,9 @@
3405 2589 }
3406 2590
3407 2591 return [];
3408 2592 }
3409 -
3410 -
2593 +// Add this to your class
3411 2594 public function handle_pdf_upload() {
3412 2595 check_ajax_referer('mxchat_chat_nonce', 'nonce');
3413 2596
3414 2597 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
@@ -3415,29 +2598,12 @@
3415 2598 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
3416 2599 return;
3417 2600 }
3418 2601
3419 - // SECURITY FIX: Check if PDF uploads are enabled in settings
3420 - $options = get_option('mxchat_options', array());
3421 - $show_pdf_button = isset($options['show_pdf_upload_button']) ? $options['show_pdf_upload_button'] : 'on';
3422 -
3423 - if ($show_pdf_button !== 'on') {
3424 - wp_send_json_error(esc_html__('PDF uploads are currently disabled.', 'mxchat'));
3425 - return;
3426 - }
3427 -
3428 2602 $file = $_FILES['pdf_file'];
3429 2603 $session_id = sanitize_text_field($_POST['session_id']);
3430 2604 $original_filename = sanitize_text_field($file['name']);
3431 2605
3432 - // Update session owner if it changed (e.g. IP changed due to network switch)
3433 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
3434 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
3435 -
3436 - if (!$session_owner || $session_owner !== $current_user_identifier) {
3437 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
3438 - }
3439 -
3440 2606 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
3441 2607 if ($file_type['type'] !== 'application/pdf') {
3442 2608 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
3443 2609 return;
@@ -3443,12 +2609,9 @@
3443 2609 return;
3444 2610 }
3445 2611
3446 2612 $upload_dir = wp_upload_dir();
3447 -
3448 - // SECURITY FIX: Generate random filename without exposing session_id
3449 - $random_string = wp_generate_password(20, false, false); // 20 char alphanumeric string
3450 - $pdf_filename = 'mxchat_' . $random_string . '_' . time() . '.pdf';
2613 + $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
3451 2614 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
3452 2615
3453 2616 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
3454 2617 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
@@ -3479,9 +2642,8 @@
3479 2642 return;
3480 2643 }
3481 2644
3482 2645 if (!empty($embeddings)) {
3483 - // Store the mapping between session and the random filename
3484 2646 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
3485 2647 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
3486 2648 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
3487 2649 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
@@ -3525,8 +2687,10 @@
3525 2687 wp_die();
3526 2688 }
3527 2689
3528 2690
2691 +
2692 +
3529 2693 function mxchat_fetch_new_messages() {
3530 2694 $session_id = sanitize_text_field($_POST['session_id']);
3531 2695 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
3532 2696 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
@@ -3539,31 +2703,14 @@
3539 2703 }
3540 2704
3541 2705 $history = get_option("mxchat_history_{$session_id}", []);
3542 2706
3543 - //error_log("MxChat WhatsApp DEBUG: Fetch new messages for session {$session_id}");
3544 - //error_log("MxChat WhatsApp DEBUG: last_seen_id = " . var_export($last_seen_id, true));
3545 - //error_log("MxChat WhatsApp DEBUG: History count = " . count($history));
3546 - //error_log("MxChat WhatsApp DEBUG: Full history = " . print_r($history, true));
3547 -
3548 2707 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
3549 - //error_log("MxChat WhatsApp DEBUG: Checking message - ID: " . ($message['id'] ?? 'NO_ID') . ", Role: " . ($message['role'] ?? 'NO_ROLE'));
3550 -
3551 2708 // If persistence is enabled, show all new messages
3552 2709 if ($persistence_enabled) {
3553 - $has_id = !empty($message['id']);
3554 - $is_agent = $message['role'] === 'agent';
3555 -
3556 - // If last_seen_id is empty, 'NaN', or invalid, show all agent messages
3557 - if (empty($last_seen_id) || $last_seen_id === 'NaN' || $last_seen_id === 'undefined') {
3558 - $is_newer = true;
3559 - } else {
3560 - $is_newer = strcmp($message['id'] ?? '', $last_seen_id) > 0;
3561 - }
3562 -
3563 - //error_log("MxChat WhatsApp DEBUG: has_id={$has_id}, is_newer={$is_newer}, is_agent={$is_agent}");
3564 -
3565 - return $has_id && $is_newer && $is_agent;
2710 + return !empty($message['id']) &&
2711 + strcmp($message['id'], $last_seen_id) > 0 &&
2712 + $message['role'] === 'agent';
3566 2713 }
3567 2714
3568 2715 // If persistence is disabled, only show messages after initial timestamp
3569 2716 return !empty($message['id']) &&
@@ -3570,16 +2717,12 @@
3570 2717 $message['role'] === 'agent' &&
3571 2718 $message['timestamp'] > $initial_timestamp;
3572 2719 });
3573 2720
3574 - //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'));
3575 2722
3576 - // Include current chat mode so frontend can detect agent→AI transitions
3577 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
3578 -
3579 2723 wp_send_json_success([
3580 - 'new_messages' => array_values($new_messages),
3581 - 'chat_mode' => $chat_mode
2724 + 'new_messages' => array_values($new_messages)
3582 2725 ]);
3583 2726 wp_die();
3584 2727 }
3585 2728 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
@@ -3864,393 +3007,9 @@
3864 3007
3865 3008 //error_log("[DEBUG] Generated channel name: {$channel_name}");
3866 3009 return $channel_name;
3867 3010 }
3868 -
3869 -/**
3870 - * Telegram Live Agent Handover
3871 - * Creates a forum topic in the Telegram group and notifies agents
3872 - */
3873 -public function mxchat_telegram_live_agent_handover($message, $user_id, $session_id) {
3874 - // Check if Telegram agents are available
3875 - $telegram_available = $this->options['telegram_status'] ?? 'off';
3876 - if ($telegram_available !== 'on') {
3877 - $away_message = $this->options['telegram_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
3878 - $this->fallbackResponse = [
3879 - 'text' => $away_message,
3880 - 'html' => '',
3881 - 'images' => [],
3882 - 'chat_mode' => 'ai'
3883 - ];
3884 - wp_send_json([
3885 - 'text' => $away_message,
3886 - 'html' => '',
3887 - 'chat_mode' => 'ai',
3888 - 'session_id' => $session_id
3889 - ]);
3890 - wp_die();
3891 - }
3892 -
3893 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
3894 - $telegram_group_id = $this->options['telegram_group_id'] ?? '';
3895 -
3896 - if (empty($telegram_bot_token) || empty($telegram_group_id)) {
3897 - return false;
3898 - }
3899 -
3900 - // Check if topic already exists for this session
3901 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
3902 -
3903 - if (empty($topic_id)) {
3904 - // Generate topic name
3905 - $topic_name = $this->generate_telegram_topic_name($session_id);
3906 -
3907 - // Random icon color (Telegram forum topic colors)
3908 - $icon_colors = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, 0xFB6F5F];
3909 - $icon_color = $icon_colors[array_rand($icon_colors)];
3910 -
3911 - // Create forum topic
3912 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/createForumTopic", [
3913 - 'headers' => ['Content-Type' => 'application/json'],
3914 - 'body' => json_encode([
3915 - 'chat_id' => $telegram_group_id,
3916 - 'name' => $topic_name,
3917 - 'icon_color' => $icon_color
3918 - ])
3919 - ]);
3920 -
3921 - if (!is_wp_error($response)) {
3922 - $response_body = wp_remote_retrieve_body($response);
3923 - $response_data = json_decode($response_body, true);
3924 -
3925 - if (isset($response_data['ok']) && $response_data['ok']) {
3926 - $topic_id = $response_data['result']['message_thread_id'];
3927 - update_option("mxchat_telegram_topic_{$session_id}", $topic_id);
3928 - update_option("mxchat_telegram_group_{$session_id}", $telegram_group_id);
3929 - }
3930 - }
3931 -
3932 - if (empty($topic_id)) {
3933 - return false; // Failed to create topic
3934 - }
3935 - }
3936 -
3937 - // Get recent chat history
3938 - $history = get_option("mxchat_history_{$session_id}", []);
3939 - $recent_history = array_slice($history, -5);
3940 -
3941 - // Format conversation context for Telegram (HTML format)
3942 - $conversation_context = "";
3943 - if (!empty($recent_history)) {
3944 - $conversation_context = "<b>Recent Conversation:</b>\n";
3945 - foreach ($recent_history as $hist_message) {
3946 - $role_display = $hist_message['role'] === 'user' ? '👤 User' : '🤖 AI';
3947 - $escaped_content = htmlspecialchars($hist_message['content'], ENT_QUOTES, 'UTF-8');
3948 - $conversation_context .= "{$role_display}: {$escaped_content}\n";
3949 - }
3950 - $conversation_context .= "\n";
3951 - }
3952 -
3953 - // Get user info
3954 - $user_email = get_option("mxchat_email_{$session_id}", 'Not provided');
3955 - $user_name = get_option("mxchat_name_{$session_id}", 'Anonymous');
3956 -
3957 - // Update session mode
3958 - update_option("mxchat_mode_{$session_id}", 'agent');
3959 -
3960 - // Send initial message to topic
3961 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3962 - $topic_message = "🔔 <b>New Live Agent Request</b>\n\n";
3963 - $topic_message .= "<b>Session ID:</b> <code>{$session_id}</code>\n";
3964 - $topic_message .= "<b>User:</b> {$user_name}\n";
3965 - $topic_message .= "<b>Email:</b> {$user_email}\n\n";
3966 -
3967 - if (!empty($conversation_context)) {
3968 - $topic_message .= $conversation_context;
3969 - }
3970 -
3971 - $topic_message .= "<b>Current Message:</b>\n{$escaped_message}\n\n";
3972 - $topic_message .= "<i>Reply in this topic - messages will be sent to the user</i>\n";
3973 - $topic_message .= "<i>Type #close, #end, #disconnect, or #done to end the session</i>";
3974 -
3975 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
3976 - 'headers' => ['Content-Type' => 'application/json'],
3977 - 'body' => json_encode([
3978 - 'chat_id' => $telegram_group_id,
3979 - 'message_thread_id' => $topic_id,
3980 - 'text' => $topic_message,
3981 - 'parse_mode' => 'HTML'
3982 - ])
3983 - ]);
3984 -
3985 - $success_message = $this->options['telegram_notification_message'] ?? "I've notified a support agent. Please allow a moment for them to respond.";
3986 - $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
3987 -
3988 - $this->fallbackResponse = [
3989 - 'text' => $success_message,
3990 - 'html' => '',
3991 - 'images' => [],
3992 - 'chat_mode' => 'agent'
3993 - ];
3994 -
3995 - wp_send_json([
3996 - 'success' => true,
3997 - 'text' => $success_message,
3998 - 'html' => '',
3999 - 'chat_mode' => 'agent',
4000 - 'session_id' => $session_id,
4001 - 'fallbackResponse' => $this->fallbackResponse
4002 - ]);
4003 - wp_die();
4004 -}
4005 -
4006 -/**
4007 - * Generate topic name for Telegram forum
4008 - */
4009 -private function generate_telegram_topic_name($session_id) {
4010 - $name = null;
4011 - $email = null;
4012 -
4013 - // Check logged in user
4014 - if (is_user_logged_in()) {
4015 - $current_user = wp_get_current_user();
4016 - if (!empty($current_user->display_name)) {
4017 - $name = $current_user->display_name;
4018 - }
4019 - if (!empty($current_user->user_email)) {
4020 - $email = $current_user->user_email;
4021 - }
4022 - }
4023 -
4024 - // Check session data
4025 - if (empty($name)) {
4026 - $name = get_option("mxchat_name_{$session_id}");
4027 - }
4028 - if (empty($email)) {
4029 - $email = get_option("mxchat_email_{$session_id}");
4030 - }
4031 -
4032 - // Generate topic name
4033 - $session_suffix = substr($session_id, -6);
4034 -
4035 - if (!empty($name)) {
4036 - // Clean name for topic (max 128 chars in Telegram)
4037 - $clean_name = preg_replace('/[^\p{L}\p{N}\s\-]/u', '', $name);
4038 - $clean_name = trim($clean_name);
4039 - if (strlen($clean_name) > 50) {
4040 - $clean_name = substr($clean_name, 0, 50);
4041 - }
4042 - return "Chat - {$clean_name} ({$session_suffix})";
4043 - } elseif (!empty($email)) {
4044 - // Use email prefix
4045 - $email_prefix = explode('@', $email)[0];
4046 - if (strlen($email_prefix) > 30) {
4047 - $email_prefix = substr($email_prefix, 0, 30);
4048 - }
4049 - return "Chat - {$email_prefix} ({$session_suffix})";
4050 - }
4051 -
4052 - return "Chat - {$session_suffix}";
4053 -}
4054 -
4055 -/**
4056 - * Send user message to Telegram agent
4057 - */
4058 -public function mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id) {
4059 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4060 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4061 - $group_id = get_option("mxchat_telegram_group_{$session_id}", '');
4062 -
4063 - if (empty($telegram_bot_token) || empty($topic_id) || empty($group_id)) {
4064 - return false;
4065 - }
4066 -
4067 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
4068 - $user_message = "👤 <b>User:</b> {$escaped_message}";
4069 -
4070 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4071 - 'headers' => ['Content-Type' => 'application/json'],
4072 - 'body' => json_encode([
4073 - 'chat_id' => $group_id,
4074 - 'message_thread_id' => $topic_id,
4075 - 'text' => $user_message,
4076 - 'parse_mode' => 'HTML'
4077 - ])
4078 - ]);
4079 -
4080 - return !is_wp_error($response);
4081 -}
4082 -
4083 -/**
4084 - * Handle incoming Telegram webhook
4085 - */
4086 -public function handle_telegram_webhook(WP_REST_Request $request) {
4087 - $body = $request->get_body();
4088 - $data = json_decode($body, true);
4089 -
4090 - //error_log('[MxChat Telegram DEBUG] Webhook received: ' . $body);
4091 -
4092 - // Handle message events from forum topics
4093 - if (isset($data['message'])) {
4094 - $message_data = $data['message'];
4095 -
4096 - // Skip if not from a forum topic
4097 - if (!isset($message_data['message_thread_id'])) {
4098 - //error_log('[MxChat Telegram DEBUG] Skipped: No message_thread_id (not a forum topic message)');
4099 - return new WP_REST_Response(['ok' => true]);
4100 - }
4101 -
4102 - // Skip bot messages
4103 - if (isset($message_data['from']['is_bot']) && $message_data['from']['is_bot']) {
4104 - //error_log('[MxChat Telegram DEBUG] Skipped: Message from bot');
4105 - return new WP_REST_Response(['ok' => true]);
4106 - }
4107 -
4108 - $chat_id = $message_data['chat']['id'] ?? '';
4109 - $topic_id = $message_data['message_thread_id'];
4110 - $message_text = $message_data['text'] ?? '';
4111 - $message_id = $message_data['message_id'] ?? '';
4112 - $from = $message_data['from'] ?? [];
4113 - $agent_name = trim(($from['first_name'] ?? '') . ' ' . ($from['last_name'] ?? ''));
4114 - if (empty($agent_name)) {
4115 - $agent_name = $from['username'] ?? 'Agent';
4116 - }
4117 -
4118 - //error_log("[MxChat Telegram DEBUG] Parsed: chat_id={$chat_id}, topic_id={$topic_id}, agent={$agent_name}, text={$message_text}");
4119 -
4120 - // Skip empty messages
4121 - if (empty($message_text)) {
4122 - //error_log('[MxChat Telegram DEBUG] Skipped: Empty message text');
4123 - return new WP_REST_Response(['ok' => true]);
4124 - }
4125 -
4126 - // Find session ID by topic ID - cast to string for comparison
4127 - global $wpdb;
4128 - $topic_id_str = strval($topic_id);
4129 - $session_option = $wpdb->get_var(
4130 - $wpdb->prepare(
4131 - "SELECT option_name FROM {$wpdb->options}
4132 - WHERE option_name LIKE %s
4133 - AND option_value = %s",
4134 - 'mxchat_telegram_topic_%',
4135 - $topic_id_str
4136 - )
4137 - );
4138 -
4139 - //error_log("[MxChat Telegram DEBUG] Looking for topic_id={$topic_id_str} in options, found: " . ($session_option ?: 'NULL'));
4140 -
4141 - if ($session_option) {
4142 - $session_id = str_replace('mxchat_telegram_topic_', '', $session_option);
4143 - //error_log("[MxChat Telegram DEBUG] Session ID: {$session_id}");
4144 -
4145 - // Verify the group ID matches
4146 - $stored_group_id = get_option("mxchat_telegram_group_{$session_id}", '');
4147 - //error_log("[MxChat Telegram DEBUG] Stored group_id={$stored_group_id}, received chat_id={$chat_id}");
4148 -
4149 - if (strval($stored_group_id) != strval($chat_id)) {
4150 - //error_log('[MxChat Telegram DEBUG] Skipped: Group ID mismatch');
4151 - return new WP_REST_Response(['ok' => true]);
4152 - }
4153 -
4154 - // Check for closure commands
4155 - $lower_text = strtolower(trim($message_text));
4156 - if (in_array($lower_text, ['#close', '#end', '#disconnect', '#done'])) {
4157 - //error_log("[MxChat Telegram DEBUG] Closure command received: {$lower_text}");
4158 - // End the live agent session
4159 - update_option("mxchat_mode_{$session_id}", 'ai');
4160 -
4161 - // Save disconnect message
4162 - $disconnect_message = "Live agent session ended. You're now chatting with the AI assistant.";
4163 - $this->mxchat_save_chat_message($session_id, 'bot', $disconnect_message);
4164 -
4165 - // Notify in Telegram
4166 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4167 - if (!empty($telegram_bot_token)) {
4168 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4169 - 'headers' => ['Content-Type' => 'application/json'],
4170 - 'body' => json_encode([
4171 - 'chat_id' => $chat_id,
4172 - 'message_thread_id' => $topic_id,
4173 - 'text' => "✅ Session closed. User returned to AI chatbot.",
4174 - 'parse_mode' => 'HTML'
4175 - ])
4176 - ]);
4177 -
4178 - // Optionally close the topic
4179 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/closeForumTopic", [
4180 - 'headers' => ['Content-Type' => 'application/json'],
4181 - 'body' => json_encode([
4182 - 'chat_id' => $chat_id,
4183 - 'message_thread_id' => $topic_id
4184 - ])
4185 - ]);
4186 - }
4187 -
4188 - return new WP_REST_Response(['ok' => true]);
4189 - }
4190 -
4191 - // Deduplicate messages
4192 - $message_key = md5($session_id . $message_id . $message_text);
4193 - $processed_messages = get_transient('mxchat_telegram_messages_' . $session_id) ?: [];
4194 -
4195 - if (in_array($message_key, $processed_messages)) {
4196 - //error_log('[MxChat Telegram DEBUG] Skipped: Duplicate message');
4197 - return new WP_REST_Response(['ok' => true]);
4198 - }
4199 -
4200 - $processed_messages[] = $message_key;
4201 - if (count($processed_messages) > 50) {
4202 - $processed_messages = array_slice($processed_messages, -50);
4203 - }
4204 - set_transient('mxchat_telegram_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4205 -
4206 - // Save the agent message - format with agent name prefix for proper parsing
4207 - $formatted_message = "Agent: {$agent_name} - {$message_text}";
4208 - //error_log("[MxChat Telegram DEBUG] Saving agent message: {$formatted_message}");
4209 -
4210 - $this->mxchat_save_chat_message($session_id, 'agent', $formatted_message);
4211 -
4212 - // Verify the message was saved to history
4213 - $history = get_option("mxchat_history_{$session_id}", []);
4214 - $last_message = end($history);
4215 - //error_log("[MxChat Telegram DEBUG] History after save - count: " . count($history) . ", last message role: " . ($last_message['role'] ?? 'none'));
4216 -
4217 - // Send confirmation back to Telegram
4218 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4219 - if (!empty($telegram_bot_token)) {
4220 - $confirm_key = 'mxchat_telegram_confirm_' . $message_key;
4221 - if (!get_transient($confirm_key)) {
4222 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4223 - 'headers' => ['Content-Type' => 'application/json'],
4224 - 'body' => json_encode([
4225 - 'chat_id' => $chat_id,
4226 - 'message_thread_id' => $topic_id,
4227 - 'text' => "✅ <i>Message sent to user</i>",
4228 - 'parse_mode' => 'HTML',
4229 - 'reply_to_message_id' => $message_id
4230 - ])
4231 - ]);
4232 - set_transient($confirm_key, true, 300);
4233 - }
4234 - }
4235 - } else {
4236 - //error_log("[MxChat Telegram DEBUG] No session found for topic_id={$topic_id}");
4237 - }
4238 - } else {
4239 - //error_log('[MxChat Telegram DEBUG] No message in webhook data');
4240 - }
4241 -
4242 - return new WP_REST_Response(['ok' => true]);
4243 -}
4244 -
4245 3011 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
4246 - // Check if this is a Telegram agent session
4247 - $telegram_topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4248 - if (!empty($telegram_topic_id)) {
4249 - return $this->mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id);
4250 - }
4251 -
4252 - // Otherwise, try Slack
4253 3012 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4254 3013 $channel_id = get_option("mxchat_channel_{$session_id}", '');
4255 3014
4256 3015 if (empty($slack_bot_token) || empty($channel_id)) {
@@ -4476,33 +3235,33 @@
4476 3235
4477 3236 $channel_id = $event['channel'];
4478 3237 $message_text = $event['text'] ?? '';
4479 3238 $message_ts = $event['ts'] ?? '';
4480 -
3239 +
4481 3240 // Find session ID by looking for matching channel
4482 3241 global $wpdb;
4483 3242 $session_option = $wpdb->get_var(
4484 3243 $wpdb->prepare(
4485 - "SELECT option_name FROM {$wpdb->options}
4486 - WHERE option_name LIKE 'mxchat_channel_%'
3244 + "SELECT option_name FROM {$wpdb->options}
3245 + WHERE option_name LIKE 'mxchat_channel_%'
4487 3246 AND option_value = %s",
4488 3247 $channel_id
4489 3248 )
4490 3249 );
4491 -
3250 +
4492 3251 if ($session_option) {
4493 3252 $session_id = str_replace('mxchat_channel_', '', $session_option);
4494 -
3253 +
4495 3254 // Create a unique key for this specific message
4496 3255 $message_key = md5($session_id . $message_ts . $message_text);
4497 3256 $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
4498 -
3257 +
4499 3258 // Check if we've already processed this exact message
4500 3259 if (in_array($message_key, $processed_messages)) {
4501 3260 //error_log("Duplicate message detected for session $session_id");
4502 3261 return new WP_REST_Response(['ok' => true]);
4503 3262 }
4504 -
3263 +
4505 3264 // Add to processed messages
4506 3265 $processed_messages[] = $message_key;
4507 3266 // Keep only last 50 messages per session
4508 3267 if (count($processed_messages) > 50) {
@@ -4508,46 +3267,14 @@
4508 3267 if (count($processed_messages) > 50) {
4509 3268 $processed_messages = array_slice($processed_messages, -50);
4510 3269 }
4511 3270 set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4512 -
4513 - $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4514 -
4515 - // Handle agent ending the chat — transfer back to AI
4516 - // Format: "!endchat" or "!endchat <custom message to user>"
4517 - if (preg_match('/^!endchat\b/i', trim($message_text))) {
4518 - update_option("mxchat_mode_{$session_id}", 'ai');
4519 -
4520 - // Extract custom message after !endchat, or use empty string
4521 - $custom_message = trim(preg_replace('/^!endchat\s*/i', '', trim($message_text)));
4522 -
4523 - // Send the agent's custom farewell message if provided
4524 - if (!empty($custom_message)) {
4525 - $this->mxchat_save_chat_message($session_id, 'agent', $custom_message);
4526 - }
4527 -
4528 - // Confirm in Slack channel
4529 - if (!empty($slack_bot_token)) {
4530 - wp_remote_post('https://slack.com/api/chat.postMessage', [
4531 - 'headers' => [
4532 - 'Content-Type' => 'application/json',
4533 - 'Authorization' => 'Bearer ' . $slack_bot_token
4534 - ],
4535 - 'body' => json_encode([
4536 - 'channel' => $channel_id,
4537 - 'text' => "✅ *Chat ended.* User has been transferred back to AI mode.",
4538 - 'mrkdwn' => true
4539 - ])
4540 - ]);
4541 - }
4542 -
4543 - return new WP_REST_Response(['ok' => true]);
4544 - }
4545 -
3271 +
4546 3272 // Save the agent message
4547 3273 $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
4548 -
3274 +
4549 3275 // Send confirmation back to Slack (only once)
3276 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4550 3277 if (!empty($slack_bot_token)) {
4551 3278 // Use a transient to prevent duplicate confirmations
4552 3279 $confirm_key = 'mxchat_confirm_' . $message_key;
4553 3280 if (!get_transient($confirm_key)) {
@@ -4557,9 +3284,9 @@
4557 3284 'Authorization' => 'Bearer ' . $slack_bot_token
4558 3285 ],
4559 3286 'body' => json_encode([
4560 3287 'channel' => $channel_id,
4561 - 'text' => "✅ _Message sent to user_",
3288 + 'text' => "✅ _Message sent to user_",
4562 3289 'thread_ts' => $event['ts'] // Reply in thread
4563 3290 ])
4564 3291 ]);
4565 3292 // Set transient to prevent duplicate confirmations
@@ -4599,15 +3326,9 @@
4599 3326 try {
4600 3327 // Get options and selected model
4601 3328 $options = get_option('mxchat_options');
4602 3329 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4603 -
4604 - // Opt-in: route embeddings through the Custom (OpenAI-compatible) provider.
4605 - // Off by default so existing sites see byte-identical behavior.
4606 - if (!empty($options['custom_provider_for_embeddings']) && $options['custom_provider_for_embeddings'] === 'on') {
4607 - return $this->mxchat_generate_embedding_custom($text);
4608 - }
4609 -
3330 +
4610 3331 // Determine endpoint and API key based on model
4611 3332 if (strpos($selected_model, 'voyage') === 0) {
4612 3333 $endpoint = 'https://api.voyageai.com/v1/embeddings';
4613 3334 $api_key = $options['voyage_api_key'] ?? '';
@@ -4804,97 +3525,26 @@
4804 3525 }
4805 3526 }
4806 3527
4807 3528
4808 -/**
4809 - * Generate embedding via a Custom (OpenAI-compatible) provider's /embeddings route.
4810 - * Only called when the opt-in 'custom_provider_for_embeddings' setting is on.
4811 - * Returns a numeric array (the embedding vector) on success, or ['error','error_code'] on failure.
4812 - */
4813 -private function mxchat_generate_embedding_custom($text) {
4814 - if (empty($text)) {
4815 - return ['error' => esc_html__('No text provided for embedding generation', 'mxchat'), 'error_code' => 'empty_embedding_text'];
4816 - }
4817 - $cfg = $this->mxchat_resolve_custom_provider();
4818 - if (empty($cfg['base_url'])) {
4819 - return ['error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat'), 'error_code' => 'missing_custom_provider_base_url'];
4820 - }
4821 -
4822 - $options = get_option('mxchat_options');
4823 - $embed_url = $cfg['base_url'] . '/embeddings';
4824 - if (!empty($cfg['api_version'])) {
4825 - $embed_url .= (strpos($embed_url, '?') === false ? '?' : '&') . 'api-version=' . rawurlencode($cfg['api_version']);
4826 - }
4827 - $model = isset($options['custom_provider_embedding_model']) && trim((string) $options['custom_provider_embedding_model']) !== ''
4828 - ? trim((string) $options['custom_provider_embedding_model'])
4829 - : $cfg['model'];
4830 -
4831 - $response = wp_remote_post($embed_url, [
4832 - 'headers' => $this->mxchat_custom_provider_assoc_headers($cfg),
4833 - 'body' => wp_json_encode(['input' => $text, 'model' => $model]),
4834 - 'timeout' => 60,
4835 - ]);
4836 - if (is_wp_error($response)) {
4837 - return [
4838 - 'error' => esc_html__('Connection error when generating embeddings (custom provider): ', 'mxchat') . esc_html($response->get_error_message()),
4839 - 'error_code' => 'embedding_custom_connection_error',
4840 - ];
4841 - }
4842 - $status = wp_remote_retrieve_response_code($response);
4843 - $body = json_decode(wp_remote_retrieve_body($response), true);
4844 - if ($status !== 200) {
4845 - $msg = isset($body['error']['message']) ? $body['error']['message'] : 'HTTP ' . $status;
4846 - return [
4847 - 'error' => esc_html__('Custom embedding endpoint error: ', 'mxchat') . esc_html($msg),
4848 - 'error_code' => 'embedding_custom_api_error',
4849 - 'status_code' => $status,
4850 - ];
4851 - }
4852 - if (isset($body['data'][0]['embedding']) && is_array($body['data'][0]['embedding'])) {
4853 - return $body['data'][0]['embedding'];
4854 - }
4855 - return [
4856 - 'error' => esc_html__('Invalid embedding response from custom provider.', 'mxchat'),
4857 - 'error_code' => 'embedding_custom_invalid_response',
4858 - ];
4859 -}
4860 -
4861 -private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default', $user_query = '') {
4862 - //error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
4863 -
4864 - // Check for OpenAI Vector Store first (takes priority when enabled)
4865 - $bot_vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
4866 -
4867 - if ($bot_vectorstore_config['use_vectorstore']) {
4868 - // Get current model to verify it's an OpenAI model
4869 - $bot_options = $this->get_bot_options($bot_id);
4870 - $mxchat_options = get_option('mxchat_options', array());
4871 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
4872 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
4873 -
4874 - if ($this->is_openai_chat_model($selected_model)) {
4875 - //error_log("MXCHAT DEBUG: Using OpenAI Vector Store for knowledge retrieval");
4876 - return $this->find_relevant_content_openai_vectorstore($user_query, $bot_id, $bot_vectorstore_config);
4877 - } else {
4878 - //error_log("MXCHAT DEBUG: Vector Store enabled but model is not OpenAI (" . $selected_model . "), skipping Vector Store");
4879 - }
4880 - }
4881 -
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 +
4882 3532 // Get bot-specific Pinecone configuration
4883 3533 $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
4884 -
3534 +
4885 3535 // Debug: Log the Pinecone configuration
4886 - //error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
4887 - //error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
4888 - //error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
4889 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
4890 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
4891 -
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 +
4892 3542 // Determine whether to use Pinecone based on bot configuration
4893 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");
4894 3546
4895 - //error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
4896 -
4897 3547 if ($use_pinecone) {
4898 3548 return $this->find_relevant_content_pinecone($user_embedding, $bot_id, $bot_pinecone_config);
4899 3549 } else {
4900 3550 return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
@@ -4903,483 +3553,233 @@
4903 3553
4904 3554 private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
4905 3555 global $wpdb;
4906 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 +
4907 3560 // Initialize similarity analysis storage
4908 3561 $this->last_similarity_analysis = [
4909 3562 'knowledge_base_type' => 'WordPress Database',
4910 - 'bot_id' => $bot_id,
3563 + 'bot_id' => $bot_id, // Track which bot is being used
4911 3564 'top_matches' => [],
4912 3565 'threshold_used' => 0,
4913 3566 'total_checked' => 0
4914 3567 ];
4915 3568
4916 - // NEW: Initialize valid URLs array
4917 - $valid_urls = [];
4918 -
4919 - // Get bot-specific options for similarity threshold
3569 + // Get bot-specific options for similarity threshold
4920 3570 $bot_options = $this->get_bot_options($bot_id);
4921 3571 $current_options = !empty($bot_options) ? $bot_options : $this->options;
4922 3572
4923 - // Get knowledge manager instance for role checking
4924 - $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;
4925 3579
4926 - // Get base similarity threshold from bot options or default options
4927 - $similarity_threshold = isset($current_options['similarity_threshold'])
4928 - ? ((int) $current_options['similarity_threshold']) / 100
4929 - : 0.35;
4930 - $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 + }
4931 3590
4932 - // Precompute bot_filter once, outside the streaming loop
4933 - $bot_filter = '';
4934 - if ($bot_id !== 'default') {
4935 - $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
4936 - if ($column_exists) {
4937 - $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
4938 - }
4939 - }
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 + );
4940 3599
4941 - // ===== STREAMING TOP-K PASS =====
4942 - // Stream rows in small batches, compute cosine similarity per row, and keep only:
4943 - // - top 10 by raw similarity (for the testing/debug display panel)
4944 - // - candidates above threshold with access (capped) for context assembly
4945 - // This bounds peak memory regardless of knowledge base size and avoids loading
4946 - // article_content for every row. article_content is fetched in Phase 2 for winners only.
4947 - $batch_size = 250;
4948 - $max_candidates = 200; // safety cap, well above rag_sources_limit * max_chunks_per_source
4949 - $top_display = [];
4950 - $candidates = [];
4951 - $total_checked = 0;
4952 - $offset = 0;
3600 + $batch = $wpdb->get_results($query);
3601 + if (empty($batch)) {
3602 + break;
3603 + }
4953 3604
4954 - do {
4955 - $batch = $wpdb->get_results($wpdb->prepare(
4956 - "SELECT id, embedding_vector, source_url, role_restriction
4957 - FROM {$system_prompt_table}
4958 - WHERE 1=1 {$bot_filter}
4959 - LIMIT %d OFFSET %d",
4960 - $batch_size,
4961 - $offset
4962 - ));
3605 + $embeddings = array_merge($embeddings, $batch);
3606 + $offset += $batch_size;
3607 + unset($batch);
3608 + } while (true);
4963 3609
4964 - if (empty($batch)) {
4965 - break;
3610 + if (empty($embeddings)) {
3611 + return '';
4966 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 + }
4967 3617
4968 - foreach ($batch as $row) {
4969 - $database_embedding = $row->embedding_vector
4970 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
4971 - : null;
3618 + // Get knowledge manager instance for role checking
3619 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
4972 3620
4973 - if (!is_array($database_embedding) || !is_array($user_embedding)) {
4974 - unset($database_embedding);
4975 - continue;
4976 - }
4977 -
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)) {
4978 3638 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
4979 - unset($database_embedding);
4980 -
4981 - $role_restriction = $row->role_restriction ?? 'public';
3639 +
3640 + // Check role access
3641 + $role_restriction = $embedding->role_restriction ?? 'public';
4982 3642 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
4983 - $source_url = $row->source_url ?? '';
4984 -
4985 - // Maintain top 10 display buffer (insert-if-beats-worst)
4986 - if (count($top_display) < 10) {
4987 - $top_display[] = [
4988 - 'id' => $row->id,
4989 - 'similarity' => $similarity,
4990 - 'source_url' => $source_url,
4991 - 'role_restriction' => $role_restriction,
4992 - 'has_access' => $has_access,
4993 - ];
4994 - usort($top_display, function ($a, $b) {
4995 - return $b['similarity'] <=> $a['similarity'];
4996 - });
4997 - } elseif ($similarity > $top_display[9]['similarity']) {
4998 - $top_display[9] = [
4999 - 'id' => $row->id,
5000 - 'similarity' => $similarity,
5001 - 'source_url' => $source_url,
5002 - 'role_restriction' => $role_restriction,
5003 - 'has_access' => $has_access,
5004 - ];
5005 - usort($top_display, function ($a, $b) {
5006 - return $b['similarity'] <=> $a['similarity'];
5007 - });
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) . '...';
5008 3652 }
5009 -
5010 - // 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
5011 3668 if ($similarity >= $similarity_threshold && $has_access) {
5012 - $candidates[] = [
5013 - 'id' => $row->id,
5014 - 'similarity' => $similarity,
5015 - 'source_url' => $source_url,
3669 + $relevant_results[] = [
3670 + 'id' => $embedding->id,
3671 + 'similarity' => $similarity
5016 3672 ];
5017 3673 }
5018 -
5019 - $total_checked++;
5020 3674 }
5021 -
5022 - unset($batch);
5023 -
5024 - // Trim candidates periodically to cap memory during long scans
5025 - if (count($candidates) > $max_candidates) {
5026 - usort($candidates, function ($a, $b) {
5027 - return $b['similarity'] <=> $a['similarity'];
5028 - });
5029 - $candidates = array_slice($candidates, 0, $max_candidates);
5030 - }
5031 -
5032 - $offset += $batch_size;
5033 - } while (true);
5034 -
5035 - if ($total_checked === 0) {
5036 - $this->current_valid_urls = [];
5037 - return '';
3675 +
3676 + unset($database_embedding);
5038 3677 }
5039 3678
5040 - // Final candidates sort (best first)
5041 - if (count($candidates) > 1) {
5042 - usort($candidates, function ($a, $b) {
5043 - return $b['similarity'] <=> $a['similarity'];
5044 - });
5045 - }
5046 -
5047 - // ===== PHASE 2: FETCH ARTICLE CONTENT ONLY FOR WINNERS =====
5048 - // Gather unique IDs we actually need (top_display + candidates) and pull
5049 - // article_content in bounded IN() batches. This avoids loading content for
5050 - // every row during the similarity scan.
5051 - $needed_ids = [];
5052 - foreach ($top_display as $item) {
5053 - $needed_ids[$item['id']] = true;
5054 - }
5055 - foreach ($candidates as $item) {
5056 - $needed_ids[$item['id']] = true;
5057 - }
5058 - $needed_ids = array_keys($needed_ids);
5059 -
5060 - $content_map = [];
5061 - if (!empty($needed_ids)) {
5062 - foreach (array_chunk($needed_ids, 250) as $chunk_ids) {
5063 - $placeholders = implode(',', array_fill(0, count($chunk_ids), '%d'));
5064 - $rows = $wpdb->get_results($wpdb->prepare(
5065 - "SELECT id, article_content FROM {$system_prompt_table} WHERE id IN ($placeholders)",
5066 - ...$chunk_ids
5067 - ));
5068 - foreach ($rows as $r) {
5069 - $content_map[$r->id] = $r->article_content;
5070 - }
5071 - unset($rows);
5072 - }
5073 - }
5074 -
5075 - // Build the all_similarities display array from the top 10
5076 - $all_similarities = [];
5077 - foreach ($top_display as $item) {
5078 - $article_content_for_parse = $content_map[$item['id']] ?? '';
5079 - $parsed_for_display = MxChat_Chunker::parse_stored_chunk($article_content_for_parse);
5080 - $is_chunk = $parsed_for_display['is_chunked'];
5081 - $chunk_meta = $parsed_for_display['metadata'];
5082 -
5083 - if (!empty($item['source_url']) && $item['source_url'] !== '#') {
5084 - $source_display = $item['source_url'];
5085 - } else {
5086 - $content_preview = strip_tags($article_content_for_parse);
5087 - $content_preview = preg_replace('/\s+/', ' ', $content_preview);
5088 - $source_display = substr(trim($content_preview), 0, 50) . '...';
5089 - }
5090 -
5091 - $all_similarities[] = [
5092 - 'document_id' => $item['id'],
5093 - 'similarity' => $item['similarity'],
5094 - 'similarity_percentage' => round($item['similarity'] * 100, 2),
5095 - 'above_threshold' => $item['similarity'] >= $similarity_threshold,
5096 - 'source_display' => $source_display,
5097 - 'content_preview' => substr(strip_tags($parsed_for_display['text'] ?? ''), 0, 100) . '...',
5098 - 'used_for_context' => false,
5099 - 'role_restriction' => $item['role_restriction'],
5100 - 'has_access' => $item['has_access'],
5101 - 'filtered_out' => !$item['has_access'],
5102 - 'is_chunk' => $is_chunk,
5103 - 'chunk_index' => $is_chunk ? ($chunk_meta['chunk_index'] ?? 0) : null,
5104 - 'total_chunks' => $is_chunk ? ($chunk_meta['total_chunks'] ?? 1) : null
5105 - ];
5106 - }
5107 -
5108 - // Build url_groups from candidates for chunk reassembly
5109 - $url_groups = array();
5110 - foreach ($candidates as $cand) {
5111 - $article_content = $content_map[$cand['id']] ?? '';
5112 - $parsed = MxChat_Chunker::parse_stored_chunk($article_content);
5113 - $is_chunked = $parsed['is_chunked'];
5114 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
5115 - $text_content = $parsed['text'];
5116 -
5117 - $source_url = $cand['source_url'];
5118 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $cand['id'];
5119 -
5120 - if (!isset($url_groups[$group_key])) {
5121 - $url_groups[$group_key] = array(
5122 - 'source_url' => $source_url,
5123 - 'best_score' => 0,
5124 - 'is_chunked' => $is_chunked,
5125 - 'chunks' => array(),
5126 - 'single_text' => '',
5127 - 'single_id' => null
5128 - );
5129 - }
5130 -
5131 - if ($cand['similarity'] > $url_groups[$group_key]['best_score']) {
5132 - $url_groups[$group_key]['best_score'] = $cand['similarity'];
5133 - }
5134 -
5135 - if ($is_chunked) {
5136 - $url_groups[$group_key]['is_chunked'] = true;
5137 - $url_groups[$group_key]['chunks'][] = array(
5138 - 'id' => $cand['id'],
5139 - 'score' => $cand['similarity'],
5140 - 'chunk_index' => $chunk_index,
5141 - 'text' => $text_content
5142 - );
5143 - } else {
5144 - $url_groups[$group_key]['single_text'] = $text_content;
5145 - $url_groups[$group_key]['single_id'] = $cand['id'];
5146 - }
5147 - }
5148 -
5149 3679 // Sort ALL similarities for testing display (highest first)
5150 3680 usort($all_similarities, function ($a, $b) {
5151 3681 return $b['similarity'] <=> $a['similarity'];
5152 3682 });
5153 -
5154 - // Sort URL groups by best score (highest first)
5155 - uasort($url_groups, function($a, $b) {
5156 - 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'];
5157 3687 });
5158 -
5159 - // Get RAG sources limit from options (default 6, min 3, max 10)
5160 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
5161 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
5162 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
5163 -
5164 - // Take top N unique URLs based on user setting
5165 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5166 -
5167 - // 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
5168 3693 $used_document_ids = [];
5169 - foreach ($top_urls as $group) {
5170 - if ($group['is_chunked']) {
5171 - foreach ($group['chunks'] as $chunk) {
5172 - $used_document_ids[] = $chunk['id'];
5173 - }
5174 - } elseif ($group['single_id']) {
5175 - $used_document_ids[] = $group['single_id'];
5176 - }
3694 + foreach ($top_results as $result) {
3695 + $used_document_ids[] = $result['id'];
5177 3696 }
5178 -
3697 +
5179 3698 // Update the all_similarities array to mark which were actually used
5180 3699 foreach ($all_similarities as &$similarity_item) {
5181 3700 $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
5182 3701 }
5183 -
5184 - // Store top 10 for testing panel
3702 +
3703 + // Store top 10 for testing panel (now with correct used_for_context flags and role info)
5185 3704 $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
5186 - $this->last_similarity_analysis['total_checked'] = $total_checked;
5187 -
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 +
5188 3709 // Initialize final content
5189 3710 $content = '';
5190 - $matches_used = 0;
5191 - $total_chunks_used = 0;
5192 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5193 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5194 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5195 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5196 -
5197 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5198 - // Use fresh options to ensure we get the latest setting value
5199 - $fresh_options = get_option('mxchat_options', []);
5200 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5201 -
5202 - // Build content from top sources
5203 - foreach ($top_urls as $group_key => $group) {
5204 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5205 -
5206 - // Stop if we've hit the total chunk limit
5207 - if ($total_chunks_used >= $max_total_chunks) {
5208 - 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;
5209 3719 }
5210 -
5211 - $full_text = '';
5212 - $chunks_in_this_source = 1; // Default for non-chunked content
5213 -
5214 - if ($group['is_chunked']) {
5215 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5216 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5217 -
5218 - // Fetch chunks for this URL with limit
5219 - $full_text = $this->reassemble_chunks_from_wordpress($source_url, $chunks_remaining, $chunks_in_this_source);
5220 -
5221 - // If fetching all chunks fails, fall back to matched chunks
5222 - if (empty($full_text)) {
5223 - // Sort matched chunks by index and concatenate
5224 - usort($group['chunks'], function($a, $b) {
5225 - return $a['chunk_index'] <=> $b['chunk_index'];
5226 - });
5227 -
5228 - $chunk_texts = array();
5229 - $chunks_in_this_source = 0;
5230 - foreach ($group['chunks'] as $chunk) {
5231 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5232 - break;
5233 - }
5234 - $chunk_texts[] = $chunk['text'];
5235 - $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;
5236 3746 }
5237 - $full_text = implode("\n\n", $chunk_texts);
5238 3747 }
5239 - } else {
5240 - $full_text = $group['single_text'];
5241 - $chunks_in_this_source = 1;
5242 - }
5243 -
5244 - if (!empty($full_text)) {
5245 - // Strip URLs from content if citation links are disabled
5246 - if (!$citation_links_enabled) {
5247 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5248 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
5249 - }
5250 -
5251 - // Use numbered reference for URL-based entries, plain info label for manual entries
5252 - // Manual entries are stored with an internal mxchat:// placeholder URL — never expose them as citations
5253 - if (!empty($source_url) && $source_url !== '#' && strpos($source_url, 'mxchat://') !== 0) {
5254 - $matches_used++;
5255 - $content .= "## Reference " . $matches_used . " ##\n";
5256 - $content .= $full_text . "\n\n";
5257 -
5258 - // Only include citation URLs if citation links are enabled
5259 - if ($citation_links_enabled) {
5260 - $valid_urls[] = $source_url;
5261 - $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;
5262 3755 }
5263 - } else {
5264 - // Manual entry — no reference number, no citation
5265 - $content .= "## Information ##\n";
5266 - $content .= $full_text . "\n\n";
5267 3756 }
5268 -
5269 - // Extract any URLs from the text content itself (only if citation links enabled)
5270 - if ($citation_links_enabled) {
5271 - preg_match_all(
5272 - '#\bhttps?://[^\s<>"\']+#i',
5273 - $full_text,
5274 - $content_urls
5275 - );
5276 - if (!empty($content_urls[0])) {
5277 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5278 - }
5279 - }
5280 -
5281 - $total_chunks_used += $chunks_in_this_source;
5282 3757 }
5283 3758 }
5284 -
5285 - // NEW: Store unique valid URLs for validation
5286 - $this->current_valid_urls = array_unique($valid_urls);
5287 -
5288 - // Store sources and chunks counts for testing/transcript display
5289 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5290 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5291 -
5292 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5293 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5294 -
3759 +
5295 3760 // Add response guidelines
5296 - if (empty($top_urls)) {
3761 + if (empty($top_results)) {
5297 3762 $content = "No reference information was found for this query.\n\n";
5298 3763 } else {
5299 - // Build response guidelines based on citation links setting
5300 3764 $content .= "\n## Response Guidelines ##\n" .
5301 3765 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5302 3766 "Be conversational and friendly, but never mention your knowledge base or training data. " .
5303 3767 "If you don't have specific information or are uncertain about any details, it's always " .
5304 3768 "better to honestly say you don't know rather than making up or guessing at answers. " .
5305 - "When information is incomplete, let them know you are unsure.\n\n";
5306 -
5307 - // Only add hyperlink instructions if citation links are enabled
5308 - if ($citation_links_enabled) {
5309 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5310 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5311 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
5312 - } else {
5313 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5314 - "Simply provide helpful answers based on the reference information without citing sources.";
5315 - }
3769 + "When information is incomplete, let them know you are unsure.";
5316 3770 }
5317 3771
5318 3772 return trim($content);
5319 3773 }
5320 3774
5321 -/**
5322 - * Fetch and reassemble chunks for a URL from WordPress database
5323 - *
5324 - * @param string $source_url The source URL to fetch chunks for
5325 - * @param int $max_chunks Maximum number of chunks to return (0 = unlimited)
5326 - * @param int &$chunk_count Reference to store the actual number of chunks returned
5327 - * @return string Reassembled content from chunks
5328 - */
5329 -private function reassemble_chunks_from_wordpress($source_url, $max_chunks = 0, &$chunk_count = 0) {
5330 - global $wpdb;
5331 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
5332 -
5333 - // Fetch all rows with this source_url
5334 - $rows = $wpdb->get_results($wpdb->prepare(
5335 - "SELECT article_content FROM {$table}
5336 - WHERE source_url = %s
5337 - ORDER BY id ASC",
5338 - $source_url
5339 - ));
5340 -
5341 - if (empty($rows)) {
5342 - $chunk_count = 0;
5343 - return '';
5344 - }
5345 -
5346 - // Parse and sort chunks by index
5347 - $chunks = array();
5348 - foreach ($rows as $row) {
5349 - $parsed = MxChat_Chunker::parse_stored_chunk($row->article_content);
5350 -
5351 - if ($parsed['is_chunked']) {
5352 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
5353 - $chunks[$chunk_index] = $parsed['text'];
5354 - } else {
5355 - // Non-chunked content - just return it
5356 - $chunks[] = $parsed['text'];
5357 - }
5358 - }
5359 -
5360 - // Sort by chunk index
5361 - ksort($chunks);
5362 -
5363 - // Apply chunk limit if specified
5364 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5365 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5366 - }
5367 -
5368 - // Store actual chunk count
5369 - $chunk_count = count($chunks);
5370 -
5371 - // Reassemble content
5372 - return implode("\n\n", $chunks);
5373 -}
5374 -
5375 3775 private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
5376 3776 global $wpdb;
5377 3777
5378 - //error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
5379 - //error_log(" - bot_id: " . $bot_id);
5380 - //error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
5381 - //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'));
5382 3782
5383 3783 // Use bot-specific config or fall back to default
5384 3784 if ($bot_config === null) {
5385 3785 $bot_config = $this->get_bot_pinecone_config($bot_id);
@@ -5388,12 +3788,12 @@
5388 3788 $api_key = $bot_config['api_key'] ?? '';
5389 3789 $host = $bot_config['host'] ?? '';
5390 3790 $namespace = $bot_config['namespace'] ?? '';
5391 3791
5392 - //error_log("MXCHAT DEBUG: Pinecone query parameters:");
5393 - //error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
5394 - //error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
5395 - //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));
5396 3796
5397 3797 // Initialize similarity analysis storage
5398 3798 $this->last_similarity_analysis = [
5399 3799 'knowledge_base_type' => 'Pinecone',
@@ -5403,17 +3803,12 @@
5403 3803 'threshold_used' => 0,
5404 3804 'total_checked' => 0
5405 3805 ];
5406 3806
5407 - // NEW: Initialize valid URLs array
5408 - $valid_urls = [];
5409 -
5410 3807 if (empty($host) || empty($api_key)) {
5411 - //error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
5412 - //error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
5413 - //error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
5414 - // Store empty array for valid URLs since we can't proceed
5415 - $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'));
5416 3811 return '';
5417 3812 }
5418 3813
5419 3814 // Get knowledge manager instance for role checking
@@ -5433,9 +3828,9 @@
5433 3828 $api_endpoint = "https://{$host}/query";
5434 3829
5435 3830 $request_body = array(
5436 3831 'vector' => $user_embedding,
5437 - '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
5438 3833 'includeMetadata' => true,
5439 3834 'includeValues' => true
5440 3835 );
5441 3836
@@ -5443,11 +3838,11 @@
5443 3838 if (!empty($namespace)) {
5444 3839 $request_body['namespace'] = $namespace;
5445 3840 }
5446 3841
5447 - //error_log("MXCHAT DEBUG: About to call Pinecone API");
5448 - //error_log(" - Endpoint: " . $api_endpoint);
5449 - //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'));
5450 3845
5451 3846 $response = wp_remote_post($api_endpoint, array(
5452 3847 'headers' => array(
5453 3848 'Api-Key' => $api_key,
@@ -5458,61 +3853,53 @@
5458 3853 'timeout' => 30
5459 3854 ));
5460 3855
5461 3856 if (is_wp_error($response)) {
5462 - //error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
5463 - // Store empty array for valid URLs
5464 - $this->current_valid_urls = [];
3857 + error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
5465 3858 return '';
5466 3859 }
5467 3860
5468 3861 $response_code = wp_remote_retrieve_response_code($response);
5469 - //error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
3862 + error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
5470 3863
5471 3864 if ($response_code !== 200) {
5472 3865 $response_body = wp_remote_retrieve_body($response);
5473 - //error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
5474 - // Store empty array for valid URLs
5475 - $this->current_valid_urls = [];
3866 + error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
5476 3867 return '';
5477 3868 }
5478 3869
5479 3870 // ADD DETAILED DEBUG SECTION HERE
5480 3871 $response_body = wp_remote_retrieve_body($response);
5481 - //error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
3872 + error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
5482 3873
5483 3874 $results = json_decode($response_body, true);
5484 3875
5485 3876 if (json_last_error() !== JSON_ERROR_NONE) {
5486 - //error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
5487 - //error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
5488 - // Store empty array for valid URLs
5489 - $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));
5490 3879 return '';
5491 3880 }
5492 3881
5493 - //error_log("MXCHAT DEBUG: Pinecone response structure:");
5494 - //error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
5495 - //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'));
5496 3885
5497 3886 if (empty($results['matches'])) {
5498 - //error_log("MXCHAT DEBUG: No matches found in Pinecone response");
5499 - //error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
5500 - // Store empty array for valid URLs
5501 - $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)));
5502 3889 return '';
5503 3890 }
5504 3891
5505 - //error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
3892 + error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
5506 3893
5507 3894 // Log first match details for debugging
5508 3895 if (!empty($results['matches'][0])) {
5509 3896 $first_match = $results['matches'][0];
5510 - //error_log("MXCHAT DEBUG: First match details:");
5511 - //error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
5512 - //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'));
5513 3900 if (isset($first_match['metadata'])) {
5514 - //error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
3901 + error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
5515 3902 }
5516 3903 }
5517 3904
5518 3905 // Initialize the final content
@@ -5518,182 +3905,45 @@
5518 3905 // Initialize the final content
5519 3906 $content = '';
5520 3907 $matches_used = 0;
5521 3908 $matches_used_for_context = [];
5522 - $total_chunks_used = 0;
5523 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5524 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5525 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5526 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5527 -
5528 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5529 - // Use fresh options to ensure we get the latest setting value
5530 - $fresh_options = get_option('mxchat_options', []);
5531 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5532 -
5533 - // NEW CHUNKING LOGIC: Group results by source_url for chunk reassembly
5534 - $url_groups = array();
5535 -
3909 +
3910 + // Process each match for actual content generation (lazy role checking)
5536 3911 foreach ($results['matches'] as $index => $match) {
5537 3912 // Skip if similarity is below threshold
5538 3913 if ($match['score'] < $similarity_threshold) {
5539 3914 continue;
5540 3915 }
5541 -
5542 - $metadata = $match['metadata'] ?? array();
5543 - $source_url = $metadata['source_url'] ?? '';
5544 - $match_id = $match['id'] ?? '';
5545 -
5546 - // LAZY ROLE CHECK: Only check role for content we're actually considering
5547 - $role_restriction = $this->get_single_vector_role($match_id, $metadata);
5548 - $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
5549 -
5550 - // Skip if user doesn't have access
5551 - if (!$has_access) {
5552 - continue;
5553 - }
5554 -
5555 - // Use a unique key for manual entries without a source URL
5556 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $match_id;
5557 -
5558 - // Group by source URL (or unique key for manual entries)
5559 - if (!isset($url_groups[$group_key])) {
5560 - $url_groups[$group_key] = array(
5561 - 'source_url' => $source_url,
5562 - 'best_score' => 0,
5563 - 'is_chunked' => isset($metadata['is_chunked']) && $metadata['is_chunked'],
5564 - 'chunks' => array(),
5565 - 'single_text' => ''
5566 - );
5567 - }
5568 -
5569 - // Track best score for this group
5570 - if ($match['score'] > $url_groups[$group_key]['best_score']) {
5571 - $url_groups[$group_key]['best_score'] = $match['score'];
5572 - }
5573 -
5574 - // Store chunk info or single text
5575 - if ($url_groups[$group_key]['is_chunked']) {
5576 - $url_groups[$group_key]['chunks'][] = array(
5577 - 'id' => $match_id,
5578 - 'score' => $match['score'],
5579 - 'chunk_index' => $metadata['chunk_index'] ?? 0,
5580 - 'text' => $metadata['text'] ?? ''
5581 - );
5582 - } else {
5583 - // Non-chunked content - just store the text
5584 - $url_groups[$group_key]['single_text'] = $metadata['text'] ?? '';
5585 - $url_groups[$group_key]['single_id'] = $match_id;
5586 - }
5587 - }
5588 -
5589 - // Sort URL groups by best score (highest first)
5590 - uasort($url_groups, function($a, $b) {
5591 - return $b['best_score'] <=> $a['best_score'];
5592 - });
5593 -
5594 - // Get RAG sources limit from options (default 6, min 3, max 10)
5595 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
5596 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
5597 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
5598 -
5599 - // Take top N unique URLs based on user setting
5600 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5601 -
5602 - // Track which match IDs are actually used for context
5603 - foreach ($top_urls as $group) {
5604 - if ($group['is_chunked']) {
5605 - foreach ($group['chunks'] as $chunk) {
5606 - $matches_used_for_context[] = $chunk['id'];
5607 - }
5608 - } elseif (!empty($group['single_id'])) {
5609 - $matches_used_for_context[] = $group['single_id'];
5610 - }
5611 - }
5612 -
5613 - // Build content from top sources
5614 - foreach ($top_urls as $group_key => $group) {
5615 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5616 -
5617 - // Stop if we've hit the total chunk limit
5618 - if ($total_chunks_used >= $max_total_chunks) {
3916 +
3917 + // Limit to top 5 matches above threshold
3918 + if ($matches_used >= 5) {
5619 3919 break;
5620 3920 }
5621 -
5622 - $full_text = '';
5623 - $chunks_in_this_source = 1; // Default for non-chunked content
5624 -
5625 - if ($group['is_chunked']) {
5626 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5627 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5628 -
5629 - // Fetch chunks for this URL with limit
5630 - $full_text = $this->reassemble_chunks_from_pinecone($source_url, $bot_config, $chunks_remaining, $chunks_in_this_source);
5631 -
5632 - // If fetching all chunks fails, fall back to matched chunks
5633 - if (empty($full_text)) {
5634 - // Sort matched chunks by index and concatenate
5635 - usort($group['chunks'], function($a, $b) {
5636 - return $a['chunk_index'] <=> $b['chunk_index'];
5637 - });
5638 -
5639 - $chunk_texts = array();
5640 - $chunks_in_this_source = 0;
5641 - foreach ($group['chunks'] as $chunk) {
5642 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5643 - break;
5644 - }
5645 - $chunk_texts[] = $chunk['text'];
5646 - $chunks_in_this_source++;
5647 - }
5648 - $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;
5649 3931 }
5650 - } else {
5651 - $full_text = $group['single_text'];
5652 - $chunks_in_this_source = 1;
5653 - }
5654 -
5655 - if (!empty($full_text)) {
5656 - // Strip URLs from content if citation links are disabled
5657 - if (!$citation_links_enabled) {
5658 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5659 - $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";
5660 3939 }
5661 -
5662 - // Use numbered reference for URL-based entries, plain info label for manual entries
5663 - // Manual entries are stored with an internal mxchat:// placeholder URL — never expose them as citations
5664 - if (!empty($source_url) && $source_url !== '#' && strpos($source_url, 'mxchat://') !== 0) {
5665 - $matches_used++;
5666 - $content .= "## Reference " . $matches_used . " ##\n";
5667 - $content .= $full_text . "\n\n";
5668 -
5669 - // Only include citation URLs if citation links are enabled
5670 - if ($citation_links_enabled) {
5671 - $valid_urls[] = $source_url;
5672 - $content .= "URL: " . $source_url . "\n\n";
5673 - }
5674 - } else {
5675 - // Manual entry — no reference number, no citation
5676 - $content .= "## Information ##\n";
5677 - $content .= $full_text . "\n\n";
5678 - }
5679 -
5680 - // Extract any URLs from the text content itself (only if citation links enabled)
5681 - if ($citation_links_enabled) {
5682 - preg_match_all(
5683 - '#\bhttps?://[^\s<>"\']+#i',
5684 - $full_text,
5685 - $content_urls
5686 - );
5687 - if (!empty($content_urls[0])) {
5688 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5689 - }
5690 - }
5691 -
5692 - $total_chunks_used += $chunks_in_this_source;
3940 +
3941 + $matches_used_for_context[] = $match['id'] ?? $index;
3942 + $matches_used++;
5693 3943 }
5694 3944 }
5695 -
3945 +
5696 3946 // Process ALL matches for testing data (top 10) - with role checking for testing display
5697 3947 $all_matches = [];
5698 3948 foreach ($results['matches'] as $index => $match) {
5699 3949 if ($index >= 10) break; // Limit to top 10 for testing
@@ -5713,19 +3963,9 @@
5713 3963 $source_display = substr(trim($content_preview), 0, 50) . '...';
5714 3964 }
5715 3965
5716 3966 $match_id_for_display = $match['id'] ?? $index;
5717 -
5718 - // Check for chunk metadata in Pinecone
5719 - $is_chunk = isset($match['metadata']['is_chunked']) && $match['metadata']['is_chunked'];
5720 - $chunk_index = isset($match['metadata']['chunk_index']) ? intval($match['metadata']['chunk_index']) : null;
5721 - $total_chunks = isset($match['metadata']['total_chunks']) ? intval($match['metadata']['total_chunks']) : null;
5722 -
5723 - // Also detect chunk from vector ID pattern: {hash}_chunk_{index}
5724 - if (!$is_chunk && MxChat_Chunker::is_chunk_vector_id($match_id_for_display)) {
5725 - $is_chunk = true;
5726 - }
5727 -
3967 +
5728 3968 $all_matches[] = [
5729 3969 'document_id' => $match_id_for_display,
5730 3970 'similarity' => $match['score'],
5731 3971 'similarity_percentage' => round($match['score'] * 100, 2),
@@ -5734,12 +3974,9 @@
5734 3974 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
5735 3975 'used_for_context' => in_array($match_id_for_display, $matches_used_for_context),
5736 3976 'role_restriction' => $role_restriction,
5737 3977 'has_access' => $has_access,
5738 - 'filtered_out' => !$has_access,
5739 - 'is_chunk' => $is_chunk,
5740 - 'chunk_index' => $chunk_index,
5741 - 'total_chunks' => $total_chunks
3978 + 'filtered_out' => !$has_access
5742 3979 ];
5743 3980 }
5744 3981
5745 3982 // Store for testing panel
@@ -5744,43 +3981,25 @@
5744 3981
5745 3982 // Store for testing panel
5746 3983 $this->last_similarity_analysis['top_matches'] = $all_matches;
5747 3984 $this->last_similarity_analysis['total_checked'] = count($results['matches']);
5748 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5749 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5750 -
5751 - // NEW: Store unique valid URLs for validation
5752 - $this->current_valid_urls = array_unique($valid_urls);
5753 -
5754 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5755 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5756 -
3985 +
5757 3986 // Add response guidelines
5758 3987 if ($matches_used === 0) {
5759 3988 $content = "No reference information was found for this query.\n\n";
5760 3989 } else {
5761 - // Build response guidelines based on citation links setting
5762 3990 $content .= "\n## Response Guidelines ##\n" .
5763 3991 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5764 3992 "Be conversational and friendly, but never mention your knowledge base or training data. " .
5765 3993 "If you don't have specific information or are uncertain about any details, it's always " .
5766 3994 "better to honestly say you don't know rather than making up or guessing at answers. " .
5767 - "When information is incomplete, let them know you are unsure.\n\n";
5768 -
5769 - // Only add hyperlink instructions if citation links are enabled
5770 - if ($citation_links_enabled) {
5771 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5772 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5773 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
5774 - } else {
5775 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5776 - "Simply provide helpful answers based on the reference information without citing sources.";
5777 - }
3995 + "When information is incomplete, let them know you are unsure.";
5778 3996 }
5779 -
3997 +
5780 3998 return trim($content);
5781 3999 }
5782 4000
4001 +
5783 4002 /**
5784 4003 * Get role restriction for a single vector (with caching)
5785 4004 */
5786 4005 private function get_single_vector_role($vector_id, $metadata = array()) {
@@ -5817,518 +4036,12 @@
5817 4036 }
5818 4037
5819 4038 // Cache individual role for 1 hour
5820 4039 wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
5821 -
4040 +
5822 4041 return $role_restriction;
5823 4042 }
5824 4043
5825 -/**
5826 - * Fetch and reassemble all chunks for a URL from Pinecone
5827 - *
5828 - * @param string $source_url The source URL to fetch chunks for
5829 - * @param array $bot_config Bot-specific Pinecone configuration
5830 - * @return string Reassembled content from all chunks
5831 - */
5832 -private function reassemble_chunks_from_pinecone($source_url, $bot_config, $max_chunks = 0, &$chunk_count = 0) {
5833 - $api_key = $bot_config['api_key'] ?? '';
5834 - $host = $bot_config['host'] ?? '';
5835 - $namespace = $bot_config['namespace'] ?? '';
5836 -
5837 - if (empty($host) || empty($api_key)) {
5838 - $chunk_count = 0;
5839 - return '';
5840 - }
5841 -
5842 - $base_hash = md5($source_url);
5843 -
5844 - // Use Pinecone list API to find all chunk vectors with this prefix
5845 - $list_url = "https://{$host}/vectors/list";
5846 -
5847 - // Limit to max_chunks if specified, otherwise fetch up to 100
5848 - $fetch_limit = ($max_chunks > 0 && $max_chunks < 100) ? $max_chunks : 100;
5849 -
5850 - $list_body = array(
5851 - 'prefix' => $base_hash . '_chunk_',
5852 - 'limit' => $fetch_limit
5853 - );
5854 -
5855 - if (!empty($namespace)) {
5856 - $list_body['namespace'] = $namespace;
5857 - }
5858 -
5859 - $list_response = wp_remote_post($list_url, array(
5860 - 'headers' => array(
5861 - 'Api-Key' => $api_key,
5862 - 'accept' => 'application/json',
5863 - 'content-type' => 'application/json'
5864 - ),
5865 - 'body' => wp_json_encode($list_body),
5866 - 'timeout' => 30
5867 - ));
5868 -
5869 - if (is_wp_error($list_response)) {
5870 - //error_log('[MXCHAT-CHUNK] List API error: ' . $list_response->get_error_message());
5871 - return '';
5872 - }
5873 -
5874 - $list_data = json_decode(wp_remote_retrieve_body($list_response), true);
5875 -
5876 - if (empty($list_data['vectors'])) {
5877 - //error_log('[MXCHAT-CHUNK] No chunk vectors found for URL: ' . $source_url);
5878 - return '';
5879 - }
5880 -
5881 - // Extract vector IDs
5882 - $vector_ids = array();
5883 - foreach ($list_data['vectors'] as $vector) {
5884 - if (isset($vector['id'])) {
5885 - $vector_ids[] = $vector['id'];
5886 - }
5887 - }
5888 -
5889 - if (empty($vector_ids)) {
5890 - return '';
5891 - }
5892 -
5893 - // Fetch all chunk content
5894 - $fetch_url = "https://{$host}/vectors/fetch";
5895 -
5896 - $fetch_body = array(
5897 - 'ids' => $vector_ids
5898 - );
5899 -
5900 - if (!empty($namespace)) {
5901 - $fetch_body['namespace'] = $namespace;
5902 - }
5903 -
5904 - $fetch_response = wp_remote_post($fetch_url, array(
5905 - 'headers' => array(
5906 - 'Api-Key' => $api_key,
5907 - 'accept' => 'application/json',
5908 - 'content-type' => 'application/json'
5909 - ),
5910 - 'body' => wp_json_encode($fetch_body),
5911 - 'timeout' => 30
5912 - ));
5913 -
5914 - if (is_wp_error($fetch_response)) {
5915 - //error_log('[MXCHAT-CHUNK] Fetch API error: ' . $fetch_response->get_error_message());
5916 - return '';
5917 - }
5918 -
5919 - $fetch_data = json_decode(wp_remote_retrieve_body($fetch_response), true);
5920 -
5921 - if (empty($fetch_data['vectors'])) {
5922 - return '';
5923 - }
5924 -
5925 - // Sort chunks by index and reassemble
5926 - $chunks = array();
5927 - foreach ($fetch_data['vectors'] as $id => $vector) {
5928 - $metadata = $vector['metadata'] ?? array();
5929 - $chunk_index = $metadata['chunk_index'] ?? 0;
5930 - $text = $metadata['text'] ?? '';
5931 -
5932 - // Store chunk with its index
5933 - $chunks[$chunk_index] = $text;
5934 - }
5935 -
5936 - // Sort by chunk index
5937 - ksort($chunks);
5938 -
5939 - // Apply chunk limit if specified
5940 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5941 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5942 - }
5943 -
5944 - // Store actual chunk count
5945 - $chunk_count = count($chunks);
5946 -
5947 - // Reassemble content
5948 - return implode("\n\n", $chunks);
5949 -}
5950 -
5951 -/**
5952 - * Search for relevant content using OpenAI Vector Store (File Search)
5953 - *
5954 - * @param string $user_query The user's query text
5955 - * @param string $bot_id The bot ID
5956 - * @param array $vectorstore_config Vector Store configuration
5957 - * @return string Formatted context string with references
5958 - */
5959 -private function find_relevant_content_openai_vectorstore($user_query, $bot_id = 'default', $vectorstore_config = array()) {
5960 - //error_log("MXCHAT DEBUG: find_relevant_content_openai_vectorstore called");
5961 - //error_log(" - bot_id: " . $bot_id);
5962 - //error_log(" - user_query length: " . strlen($user_query));
5963 -
5964 - // Get OpenAI API key
5965 - $mxchat_options = get_option('mxchat_options', array());
5966 - $api_key = $mxchat_options['api_key'] ?? '';
5967 -
5968 - // Reset vectorstore error tracking
5969 - $this->last_vectorstore_error = null;
5970 -
5971 - if (empty($api_key)) {
5972 - //error_log("MXCHAT DEBUG ERROR: OpenAI API key not configured");
5973 - $this->last_vectorstore_error = 'Vector Store search failed: OpenAI API key is not configured.';
5974 - $this->current_valid_urls = [];
5975 - return '';
5976 - }
5977 -
5978 - // Get Vector Store configuration
5979 - if (empty($vectorstore_config)) {
5980 - $vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
5981 - }
5982 -
5983 - $vectorstore_ids_string = $vectorstore_config['vectorstore_ids'] ?? '';
5984 - $max_results = $vectorstore_config['max_results'] ?? 5;
5985 -
5986 - if (empty($vectorstore_ids_string)) {
5987 - //error_log("MXCHAT DEBUG ERROR: No Vector Store IDs configured");
5988 - $this->last_vectorstore_error = 'Vector Store search failed: No Vector Store IDs are configured for this bot.';
5989 - $this->current_valid_urls = [];
5990 - return '';
5991 - }
5992 -
5993 - // Parse Vector Store IDs
5994 - $vectorstore_ids = array_map('trim', explode(',', $vectorstore_ids_string));
5995 - $vectorstore_ids = array_filter($vectorstore_ids); // Remove empty values
5996 -
5997 - //error_log("MXCHAT DEBUG: Vector Store IDs: " . implode(', ', $vectorstore_ids));
5998 - //error_log("MXCHAT DEBUG: Max results: " . $max_results);
5999 -
6000 - // Initialize similarity analysis storage
6001 - $this->last_similarity_analysis = [
6002 - 'knowledge_base_type' => 'OpenAI Vector Store',
6003 - 'bot_id' => $bot_id,
6004 - 'vectorstore_ids' => $vectorstore_ids,
6005 - 'top_matches' => [],
6006 - 'threshold_used' => 0,
6007 - 'total_checked' => 0
6008 - ];
6009 -
6010 - $valid_urls = [];
6011 -
6012 - // Get the selected model
6013 - $bot_options = $this->get_bot_options($bot_id);
6014 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
6015 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
6016 -
6017 - // Verify it's an OpenAI model
6018 - if (!$this->is_openai_chat_model($selected_model)) {
6019 - //error_log("MXCHAT DEBUG ERROR: Vector Store search requires OpenAI model. Current: " . $selected_model);
6020 - $this->last_vectorstore_error = 'Vector Store search requires an OpenAI model. Current model: ' . $selected_model;
6021 - $this->current_valid_urls = [];
6022 - return '';
6023 - }
6024 -
6025 - // Use OpenAI Responses API with file_search tool
6026 - $request_body = array(
6027 - 'model' => $selected_model,
6028 - 'input' => $user_query,
6029 - 'tools' => array(
6030 - array(
6031 - 'type' => 'file_search',
6032 - 'vector_store_ids' => $vectorstore_ids,
6033 - 'max_num_results' => intval($max_results)
6034 - )
6035 - ),
6036 - 'include' => array('output[*].file_search_call.search_results')
6037 - );
6038 -
6039 - //error_log("MXCHAT VECTORSTORE: ========== REQUEST START ==========");
6040 - //error_log("MXCHAT VECTORSTORE: Model: " . $selected_model);
6041 - //error_log("MXCHAT VECTORSTORE: Query: " . substr($user_query, 0, 200));
6042 - //error_log("MXCHAT VECTORSTORE: Vector Store IDs: " . implode(', ', $vectorstore_ids));
6043 - //error_log("MXCHAT VECTORSTORE: Max Results: " . $max_results);
6044 - //error_log("MXCHAT VECTORSTORE: Request body: " . wp_json_encode($request_body));
6045 -
6046 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
6047 - 'headers' => array(
6048 - 'Authorization' => 'Bearer ' . $api_key,
6049 - 'Content-Type' => 'application/json'
6050 - ),
6051 - 'body' => wp_json_encode($request_body),
6052 - 'timeout' => 60
6053 - ));
6054 -
6055 - if (is_wp_error($response)) {
6056 - //error_log("MXCHAT VECTORSTORE ERROR: WP Error: " . $response->get_error_message());
6057 - $this->last_vectorstore_error = 'Vector Store API request failed: ' . $response->get_error_message();
6058 - $this->current_valid_urls = [];
6059 - return '';
6060 - }
6061 -
6062 - $response_code = wp_remote_retrieve_response_code($response);
6063 - //error_log("MXCHAT VECTORSTORE: Response code: " . $response_code);
6064 -
6065 - $response_body = wp_remote_retrieve_body($response);
6066 - //error_log("MXCHAT VECTORSTORE: Raw response (first 2000 chars): " . substr($response_body, 0, 2000));
6067 -
6068 - if ($response_code !== 200) {
6069 - //error_log("MXCHAT VECTORSTORE ERROR: API error response: " . $response_body);
6070 - $api_error_detail = '';
6071 - $decoded_error = json_decode($response_body, true);
6072 - if (isset($decoded_error['error']['message'])) {
6073 - $api_error_detail = $decoded_error['error']['message'];
6074 - }
6075 - $this->last_vectorstore_error = 'Vector Store API returned HTTP ' . $response_code . ($api_error_detail ? ': ' . $api_error_detail : '');
6076 - $this->current_valid_urls = [];
6077 - return '';
6078 - }
6079 - $result = json_decode($response_body, true);
6080 -
6081 - if (json_last_error() !== JSON_ERROR_NONE) {
6082 - //error_log("MXCHAT VECTORSTORE ERROR: JSON decode error: " . json_last_error_msg());
6083 - $this->last_vectorstore_error = 'Vector Store response could not be parsed: ' . json_last_error_msg();
6084 - $this->current_valid_urls = [];
6085 - return '';
6086 - }
6087 -
6088 - // Debug: Log the structure of the result
6089 - //error_log("MXCHAT VECTORSTORE: Result keys: " . implode(', ', array_keys($result)));
6090 - if (isset($result['output'])) {
6091 - //error_log("MXCHAT VECTORSTORE: Output count: " . count($result['output']));
6092 - foreach ($result['output'] as $idx => $out) {
6093 - //error_log("MXCHAT VECTORSTORE: Output[$idx] type: " . ($out['type'] ?? 'unknown'));
6094 - //error_log("MXCHAT VECTORSTORE: Output[$idx] keys: " . implode(', ', array_keys($out)));
6095 - }
6096 - } else {
6097 - //error_log("MXCHAT VECTORSTORE: No 'output' key in result!");
6098 - }
6099 -
6100 - // Extract file search results from the response
6101 - $content = '';
6102 - $matches_used = 0;
6103 - $all_matches = [];
6104 -
6105 - // The Responses API returns output array with tool results
6106 - if (isset($result['output']) && is_array($result['output'])) {
6107 - foreach ($result['output'] as $output_item) {
6108 - // Look for file_search_call results
6109 - if (isset($output_item['type']) && $output_item['type'] === 'file_search_call') {
6110 - //error_log("MXCHAT VECTORSTORE: Found file_search_call output item");
6111 - //error_log("MXCHAT VECTORSTORE: file_search_call keys: " . implode(', ', array_keys($output_item)));
6112 -
6113 - // Check for search_results in the output item directly
6114 - $search_results = $output_item['search_results'] ?? $output_item['results'] ?? [];
6115 - //error_log("MXCHAT VECTORSTORE: Search results count: " . count($search_results));
6116 -
6117 - if (empty($search_results)) {
6118 - //error_log("MXCHAT VECTORSTORE: No search results found in file_search_call");
6119 - //error_log("MXCHAT VECTORSTORE: file_search_call content: " . wp_json_encode($output_item));
6120 - }
6121 -
6122 - foreach ($search_results as $index => $search_result) {
6123 - $filename = $search_result['filename'] ?? '';
6124 - $score = $search_result['score'] ?? 0;
6125 - $text_content = '';
6126 -
6127 - // Extract text content from the result
6128 - // The text can be directly on the result OR nested under content array
6129 - if (isset($search_result['text']) && !empty($search_result['text'])) {
6130 - // Direct text field (OpenAI's actual format)
6131 - $text_content = $search_result['text'];
6132 - //error_log("MXCHAT VECTORSTORE: Found text directly on result[$index], length: " . strlen($text_content));
6133 - } elseif (isset($search_result['content']) && is_array($search_result['content'])) {
6134 - // Nested content array format
6135 - foreach ($search_result['content'] as $content_item) {
6136 - if (isset($content_item['text'])) {
6137 - $text_content .= $content_item['text'] . "\n";
6138 - }
6139 - }
6140 - //error_log("MXCHAT VECTORSTORE: Found text in content array for result[$index], length: " . strlen($text_content));
6141 - } else {
6142 - //error_log("MXCHAT VECTORSTORE: No text found for result[$index]. Keys: " . implode(', ', array_keys($search_result)));
6143 - }
6144 -
6145 - if (!empty($text_content)) {
6146 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
6147 - $content .= trim($text_content) . "\n\n";
6148 -
6149 - if (!empty($filename)) {
6150 - $content .= "Source: " . $filename . "\n\n";
6151 - }
6152 -
6153 - // Extract URLs from content
6154 - preg_match_all(
6155 - '#\bhttps?://[^\s<>"\']+#i',
6156 - $text_content,
6157 - $content_urls
6158 - );
6159 - if (!empty($content_urls[0])) {
6160 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6161 - }
6162 -
6163 - $matches_used++;
6164 - }
6165 -
6166 - // Store for similarity analysis
6167 - $all_matches[] = [
6168 - 'document_id' => $filename ?: ('result_' . $index),
6169 - 'similarity' => $score,
6170 - 'similarity_percentage' => round($score * 100, 2),
6171 - 'above_threshold' => true,
6172 - 'source_display' => $filename,
6173 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6174 - 'used_for_context' => true,
6175 - 'role_restriction' => 'public',
6176 - 'has_access' => true,
6177 - 'filtered_out' => false
6178 - ];
6179 - }
6180 - }
6181 -
6182 - // Also check for message content with annotations (citations)
6183 - if (isset($output_item['type']) && $output_item['type'] === 'message') {
6184 - if (isset($output_item['content']) && is_array($output_item['content'])) {
6185 - foreach ($output_item['content'] as $content_block) {
6186 - if (isset($content_block['annotations']) && is_array($content_block['annotations'])) {
6187 - foreach ($content_block['annotations'] as $annotation) {
6188 - if (isset($annotation['filename'])) {
6189 - $filename = $annotation['filename'];
6190 - $score = $annotation['score'] ?? 0;
6191 - $text_content = '';
6192 -
6193 - if (isset($annotation['content']) && is_array($annotation['content'])) {
6194 - foreach ($annotation['content'] as $ann_content) {
6195 - if (isset($ann_content['text'])) {
6196 - $text_content .= $ann_content['text'] . "\n";
6197 - }
6198 - }
6199 - }
6200 -
6201 - if (!empty($text_content) && $matches_used < $max_results) {
6202 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
6203 - $content .= trim($text_content) . "\n\n";
6204 - $content .= "Source: " . $filename . "\n\n";
6205 -
6206 - preg_match_all(
6207 - '#\bhttps?://[^\s<>"\']+#i',
6208 - $text_content,
6209 - $content_urls
6210 - );
6211 - if (!empty($content_urls[0])) {
6212 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6213 - }
6214 -
6215 - $matches_used++;
6216 -
6217 - $all_matches[] = [
6218 - 'document_id' => $filename,
6219 - 'similarity' => $score,
6220 - 'similarity_percentage' => round($score * 100, 2),
6221 - 'above_threshold' => true,
6222 - 'source_display' => $filename,
6223 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6224 - 'used_for_context' => true,
6225 - 'role_restriction' => 'public',
6226 - 'has_access' => true,
6227 - 'filtered_out' => false
6228 - ];
6229 - }
6230 - }
6231 - }
6232 - }
6233 - }
6234 - }
6235 - }
6236 - }
6237 - }
6238 -
6239 - // Store for testing panel
6240 - $this->last_similarity_analysis['top_matches'] = $all_matches;
6241 - $this->last_similarity_analysis['total_checked'] = count($all_matches);
6242 -
6243 - // Store unique valid URLs for validation
6244 - $this->current_valid_urls = array_unique($valid_urls);
6245 -
6246 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
6247 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
6248 -
6249 - //error_log("MXCHAT VECTORSTORE: ========== SEARCH COMPLETE ==========");
6250 - //error_log("MXCHAT VECTORSTORE: Matches used: " . $matches_used);
6251 - //error_log("MXCHAT VECTORSTORE: All matches count: " . count($all_matches));
6252 - //error_log("MXCHAT VECTORSTORE: Content length: " . strlen($content));
6253 - if ($matches_used > 0) {
6254 - //error_log("MXCHAT VECTORSTORE: Content preview: " . substr($content, 0, 500));
6255 - }
6256 -
6257 - // Check if citation links are enabled
6258 - $citation_links_enabled = ($mxchat_options['citation_links_toggle'] ?? 'on') === 'on';
6259 -
6260 - // Add response guidelines
6261 - if ($matches_used === 0) {
6262 - //error_log("MXCHAT VECTORSTORE: No matches found - returning empty reference message");
6263 - $content = "No reference information was found for this query.\n\n";
6264 - } else {
6265 - // Build response guidelines based on citation links setting
6266 - $content .= "\n## Response Guidelines ##\n" .
6267 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
6268 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
6269 - "If you don't have specific information or are uncertain about any details, it's always " .
6270 - "better to honestly say you don't know rather than making up or guessing at answers. " .
6271 - "When information is incomplete, let them know you are unsure.\n\n";
6272 -
6273 - // Only add hyperlink instructions if citation links are enabled
6274 - if ($citation_links_enabled) {
6275 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
6276 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
6277 - } else {
6278 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
6279 - "Simply provide helpful answers based on the reference information without citing sources.";
6280 - }
6281 - }
6282 -
6283 - //error_log("MXCHAT DEBUG: Vector Store search complete. Matches used: " . $matches_used);
6284 -
6285 - return trim($content);
6286 -}
6287 -
6288 -/**
6289 - * Check if the given model is an OpenAI chat model
6290 - *
6291 - * @param string $model The model ID
6292 - * @return bool True if it's an OpenAI model
6293 - */
6294 -private function is_openai_chat_model($model) {
6295 - $openai_prefixes = array('gpt-', 'o1-', 'o3-');
6296 - foreach ($openai_prefixes as $prefix) {
6297 - if (strpos($model, $prefix) === 0) {
6298 - return true;
6299 - }
6300 - }
6301 - return false;
6302 -}
6303 -
6304 -/**
6305 - * Get bot-specific Vector Store configuration
6306 - *
6307 - * @param string $bot_id The bot ID
6308 - * @return array Configuration array
6309 - */
6310 -private function get_bot_vectorstore_config($bot_id = 'default') {
6311 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
6312 -
6313 - // Default global settings
6314 - $default_config = array(
6315 - 'use_vectorstore' => ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1',
6316 - 'vectorstore_ids' => $vectorstore_options['mxchat_vectorstore_ids'] ?? '',
6317 - 'max_results' => $vectorstore_options['mxchat_vectorstore_max_results'] ?? 5
6318 - );
6319 -
6320 - // Allow multi-bot plugin to override with bot-specific settings
6321 - $bot_config = apply_filters('mxchat_get_bot_vectorstore_config', $default_config, $bot_id);
6322 -
6323 - // Preserve max_results from global settings if not set in bot config
6324 - if (!isset($bot_config['max_results'])) {
6325 - $bot_config['max_results'] = $default_config['max_results'];
6326 - }
6327 -
6328 - return $bot_config;
6329 -}
6330 -
6331 4044 private function mxchat_find_relevant_products($user_embedding) {
6332 4045 //error_log('MXChat Vector Search: Starting product search...');
6333 4046
6334 4047 // Retrieve the add-on settings from the database
@@ -6349,75 +4062,73 @@
6349 4062 }
6350 4063 private function find_relevant_products_wordpress($user_embedding) {
6351 4064 global $wpdb;
6352 4065 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
4066 + $cache_key = 'mxchat_system_prompt_embeddings';
4067 + $batch_size = 500;
6353 4068
6354 - if (!is_array($user_embedding)) {
6355 - return '';
6356 - }
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;
6357 4075
6358 - // Streaming top-K pass: scan rows in small batches, keep only the top 3
6359 - // results above the similarity threshold. Peak memory is bounded by
6360 - // $batch_size embedding rows plus a 3-element top list.
6361 - $batch_size = 250;
6362 - $similarity_threshold = 0.85;
6363 - $top_k = 3;
6364 - $top_results = [];
6365 - $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 + );
6366 4084
6367 - do {
6368 - $batch = $wpdb->get_results($wpdb->prepare(
6369 - "SELECT id, embedding_vector
6370 - FROM {$system_prompt_table}
6371 - LIMIT %d OFFSET %d",
6372 - $batch_size,
6373 - $offset
6374 - ));
4085 + $batch = $wpdb->get_results($query);
4086 + if (empty($batch)) {
4087 + break;
4088 + }
6375 4089
6376 - if (empty($batch)) {
6377 - break;
6378 - }
4090 + $embeddings = array_merge($embeddings, $batch);
4091 + $offset += $batch_size;
6379 4092
6380 - foreach ($batch as $row) {
6381 - $database_embedding = $row->embedding_vector
6382 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
6383 - : null;
4093 + unset($batch);
6384 4094
6385 - if (!is_array($database_embedding)) {
6386 - unset($database_embedding);
6387 - continue;
6388 - }
4095 + } while (true);
6389 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)) {
6390 4109 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
6391 - unset($database_embedding);
6392 -
6393 - if ($similarity < $similarity_threshold) {
6394 - continue;
6395 - }
6396 -
6397 - // Insert into bounded top-K (kept sorted descending)
6398 - if (count($top_results) < $top_k) {
6399 - $top_results[] = ['id' => $row->id, 'similarity' => $similarity];
6400 - usort($top_results, function ($a, $b) {
6401 - return $b['similarity'] <=> $a['similarity'];
6402 - });
6403 - } elseif ($similarity > $top_results[$top_k - 1]['similarity']) {
6404 - $top_results[$top_k - 1] = ['id' => $row->id, 'similarity' => $similarity];
6405 - usort($top_results, function ($a, $b) {
6406 - return $b['similarity'] <=> $a['similarity'];
6407 - });
6408 - }
4110 + $relevant_results[] = [
4111 + 'id' => $embedding->id,
4112 + 'similarity' => $similarity
4113 + ];
6409 4114 }
4115 + unset($database_embedding);
4116 + }
6410 4117
6411 - unset($batch);
6412 - $offset += $batch_size;
6413 - } while (true);
4118 + // Use fixed threshold for products
4119 + $similarity_threshold = 0.85;
6414 4120
6415 - if (empty($top_results)) {
6416 - return '';
6417 - }
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 + });
6418 4127
4128 + $top_results = array_slice($relevant_results, 0, 5);
6419 4129 $content = '';
4130 +
6420 4131 foreach ($top_results as $result) {
6421 4132 $chunk_content = $this->fetch_content_with_product_links($result['id']);
6422 4133 $content .= $chunk_content . "\n\n";
6423 4134 }
@@ -6526,61 +4237,23 @@
6526 4237
6527 4238 /**
6528 4239 * Get system instructions for a specific bot or default
6529 4240 * Checks for multi-bot add-on and uses bot-specific instructions if available
6530 - * Automatically strips URLs if citation links are disabled
6531 - * Replaces {visitor_name} placeholder with actual visitor name if available
6532 - *
6533 - * @param string $bot_id The bot ID to get instructions for
6534 - * @param string $session_id Optional session ID to lookup visitor name
6535 4241 */
6536 -private function get_system_instructions($bot_id = 'default', $session_id = '') {
6537 - $instructions = '';
6538 -
4242 +private function get_system_instructions($bot_id = 'default') {
6539 4243 // Check if multi-bot add-on is active
6540 4244 if (class_exists('MxChat_Multi_Bot_Core_Manager') && $bot_id !== 'default') {
6541 4245 // Get bot-specific options from multi-bot add-on
6542 4246 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
6543 -
4247 +
6544 4248 // If bot has custom system instructions, use those
6545 4249 if (!empty($bot_options['system_prompt_instructions'])) {
6546 - $instructions = $bot_options['system_prompt_instructions'];
4250 + return $bot_options['system_prompt_instructions'];
6547 4251 }
6548 4252 }
6549 -
4253 +
6550 4254 // Fall back to default system instructions
6551 - if (empty($instructions)) {
6552 - $instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6553 - }
6554 -
6555 - // Check if citation links are disabled - if so, strip URLs from instructions
6556 - $fresh_options = get_option('mxchat_options', []);
6557 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
6558 -
6559 - if (!$citation_links_enabled && !empty($instructions)) {
6560 - $instructions = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $instructions);
6561 - $instructions = preg_replace('/\s+/', ' ', trim($instructions)); // Clean up extra spaces
6562 - }
6563 -
6564 - // Replace {visitor_name} placeholder with actual visitor name if available
6565 - if (!empty($instructions) && !empty($session_id) && stripos($instructions, '{visitor_name}') !== false) {
6566 - $name_option_key = "mxchat_name_{$session_id}";
6567 - $visitor_name = get_option($name_option_key, '');
6568 -
6569 - if (!empty($visitor_name)) {
6570 - $instructions = str_ireplace('{visitor_name}', sanitize_text_field($visitor_name), $instructions);
6571 - } else {
6572 - // Remove placeholder if no name is available
6573 - $instructions = str_ireplace('{visitor_name}', '', $instructions);
6574 - $instructions = preg_replace('/\s{2,}/', ' ', trim($instructions)); // Clean up extra spaces
6575 - }
6576 - }
6577 -
6578 - // Allow developers to filter system instructions and process shortcodes
6579 - $instructions = apply_filters('mxchat_system_instructions', $instructions, $bot_id, $session_id);
6580 - $instructions = do_shortcode($instructions);
6581 -
6582 - return $instructions;
4255 + return isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6583 4256 }
6584 4257 /**
6585 4258 * Get the current bot ID from session or request context
6586 4259 */
@@ -6600,9 +4273,9 @@
6600 4273
6601 4274 // Fall back to default
6602 4275 return 'default';
6603 4276 }
6604 -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') {
6605 4278 try {
6606 4279 if (!$relevant_content) {
6607 4280 $error_response = [
6608 4281 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
@@ -6608,73 +4281,22 @@
6608 4281 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
6609 4282 'error_code' => 'no_relevant_content'
6610 4283 ];
6611 4284
4285 + // Add testing data to error response if available
6612 4286 if ($testing_data !== null) {
6613 4287 $error_response['testing_data'] = $testing_data;
4288 + //error_log("MxChat Testing: Added testing data to no_relevant_content error");
6614 4289 }
6615 4290
6616 4291 return $error_response;
6617 4292 }
6618 4293
4294 + // Ensure conversation_history is an array
6619 4295 if (!is_array($conversation_history)) {
6620 4296 $conversation_history = array();
6621 4297 }
6622 4298
6623 - // Check if this is an OpenRouter model
6624 - if ($selected_model === 'openrouter') {
6625 - // Get the actual OpenRouter model from options
6626 - $openrouter_selected_model = $this->options['openrouter_selected_model'] ?? '';
6627 -
6628 - if (empty($openrouter_selected_model)) {
6629 - $error_response = [
6630 - 'error' => esc_html__('No OpenRouter model selected. Please select a model in settings.', 'mxchat'),
6631 - 'error_code' => 'no_openrouter_model_selected'
6632 - ];
6633 - if ($testing_data !== null) {
6634 - $error_response['testing_data'] = $testing_data;
6635 - }
6636 - return $error_response;
6637 - }
6638 -
6639 - if (empty($openrouter_api_key)) {
6640 - $error_response = [
6641 - 'error' => esc_html__('OpenRouter API key is not configured', 'mxchat'),
6642 - 'error_code' => 'missing_openrouter_api_key'
6643 - ];
6644 - if ($testing_data !== null) {
6645 - $error_response['testing_data'] = $testing_data;
6646 - }
6647 - return $error_response;
6648 - }
6649 -
6650 - if ($streaming) {
6651 - return $this->mxchat_generate_response_openrouter_stream(
6652 - $openrouter_selected_model,
6653 - $openrouter_api_key,
6654 - $conversation_history,
6655 - $relevant_content,
6656 - $session_id,
6657 - $testing_data
6658 - );
6659 - } else {
6660 - $response = $this->mxchat_generate_response_openrouter(
6661 - $openrouter_selected_model,
6662 - $openrouter_api_key,
6663 - $conversation_history,
6664 - $relevant_content
6665 - );
6666 - }
6667 -
6668 - if (is_array($response) && isset($response['error'])) {
6669 - if ($testing_data !== null) {
6670 - $response['testing_data'] = $testing_data;
6671 - }
6672 - return $response;
6673 - }
6674 -
6675 - return $response;
6676 - }
6677 4299
6678 4300 // Extract model prefix to determine the provider
6679 4301 $model_parts = explode('-', $selected_model);
6680 4302 $provider = strtolower($model_parts[0]);
@@ -6717,9 +4339,9 @@
6717 4339 $claude_api_key,
6718 4340 $conversation_history,
6719 4341 $relevant_content,
6720 4342 $session_id,
6721 - $testing_data
4343 + $testing_data // Pass testing data
6722 4344 );
6723 4345 } else {
6724 4346 $response = $this->mxchat_generate_response_claude(
6725 4347 $selected_model,
@@ -6747,9 +4369,9 @@
6747 4369 $xai_api_key,
6748 4370 $conversation_history,
6749 4371 $relevant_content,
6750 4372 $session_id,
6751 - $testing_data
4373 + $testing_data // Pass testing data
6752 4374 );
6753 4375 } else {
6754 4376 $response = $this->mxchat_generate_response_xai(
6755 4377 $selected_model,
@@ -6777,9 +4399,9 @@
6777 4399 $deepseek_api_key,
6778 4400 $conversation_history,
6779 4401 $relevant_content,
6780 4402 $session_id,
6781 - $testing_data
4403 + $testing_data // Pass testing data
6782 4404 );
6783 4405 } else {
6784 4406 $response = $this->mxchat_generate_response_deepseek(
6785 4407 $selected_model,
@@ -6789,38 +4411,8 @@
6789 4411 );
6790 4412 }
6791 4413 break;
6792 4414
6793 - case 'custom':
6794 - // Custom (OpenAI-compatible) provider — Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI
6795 - $cp_base_url = isset($this->options['custom_provider_base_url']) ? trim((string) $this->options['custom_provider_base_url']) : '';
6796 - if (empty($cp_base_url)) {
6797 - $error_response = [
6798 - 'error' => esc_html__('Custom provider is not configured. Set Base URL in MxChat → API Keys → Custom Provider.', 'mxchat'),
6799 - 'error_code' => 'missing_custom_provider_base_url'
6800 - ];
6801 - if ($testing_data !== null) {
6802 - $error_response['testing_data'] = $testing_data;
6803 - }
6804 - return $error_response;
6805 - }
6806 - if ($streaming) {
6807 - return $this->mxchat_generate_response_custom_stream(
6808 - $selected_model,
6809 - $conversation_history,
6810 - $relevant_content,
6811 - $session_id,
6812 - $testing_data
6813 - );
6814 - } else {
6815 - $response = $this->mxchat_generate_response_custom(
6816 - $selected_model,
6817 - $conversation_history,
6818 - $relevant_content
6819 - );
6820 - }
6821 - break;
6822 -
6823 4415 case 'gpt':
6824 4416 case 'o1':
6825 4417 if (empty($api_key)) {
6826 4418 $error_response = [
@@ -6831,27 +4423,9 @@
6831 4423 $error_response['testing_data'] = $testing_data;
6832 4424 }
6833 4425 return $error_response;
6834 4426 }
6835 -
6836 - // Check if web search is enabled for this OpenAI model
6837 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6838 - // Models that don't support web search
6839 - $unsupported_web_search_models = array('gpt-4.1-nano');
6840 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6841 -
6842 - if ($web_search_enabled && $model_supports_web_search) {
6843 - // Use Responses API (required for some models, or when web search is enabled)
6844 - return $this->mxchat_generate_response_openai_web_search(
6845 - $selected_model,
6846 - $api_key,
6847 - $conversation_history,
6848 - $relevant_content,
6849 - $session_id,
6850 - $testing_data,
6851 - $streaming
6852 - );
6853 - } elseif ($streaming) {
4427 + if ($streaming) {
6854 4428 return $this->mxchat_generate_response_openai_stream(
6855 4429 $selected_model,
6856 4430 $api_key,
6857 4431 $conversation_history,
@@ -6856,9 +4430,9 @@
6856 4430 $api_key,
6857 4431 $conversation_history,
6858 4432 $relevant_content,
6859 4433 $session_id,
6860 - $testing_data
4434 + $testing_data // Pass testing data
6861 4435 );
6862 4436 } else {
6863 4437 $response = $this->mxchat_generate_response_openai(
6864 4438 $selected_model,
@@ -6869,8 +4443,9 @@
6869 4443 }
6870 4444 break;
6871 4445
6872 4446 default:
4447 + // Default to OpenAI for custom models or unrecognized prefixes
6873 4448 if (empty($api_key)) {
6874 4449 $error_response = [
6875 4450 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
6876 4451 'error_code' => 'missing_openai_api_key'
@@ -6879,25 +4454,9 @@
6879 4454 $error_response['testing_data'] = $testing_data;
6880 4455 }
6881 4456 return $error_response;
6882 4457 }
6883 -
6884 - // Check if web search is enabled (default case also handles OpenAI models)
6885 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6886 - $unsupported_web_search_models = array('gpt-4.1-nano');
6887 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6888 -
6889 - if ($web_search_enabled && $model_supports_web_search) {
6890 - return $this->mxchat_generate_response_openai_web_search(
6891 - $selected_model,
6892 - $api_key,
6893 - $conversation_history,
6894 - $relevant_content,
6895 - $session_id,
6896 - $testing_data,
6897 - $streaming
6898 - );
6899 - } elseif ($streaming) {
4458 + if ($streaming) {
6900 4459 return $this->mxchat_generate_response_openai_stream(
6901 4460 $selected_model,
6902 4461 $api_key,
6903 4462 $conversation_history,
@@ -6902,9 +4461,9 @@
6902 4461 $api_key,
6903 4462 $conversation_history,
6904 4463 $relevant_content,
6905 4464 $session_id,
6906 - $testing_data
4465 + $testing_data // Pass testing data
6907 4466 );
6908 4467 } else {
6909 4468 $response = $this->mxchat_generate_response_openai(
6910 4469 $selected_model,
@@ -6915,18 +4474,24 @@
6915 4474 }
6916 4475 break;
6917 4476 }
6918 4477
4478 + // Check if the response is an error array from the provider-specific function
6919 4479 if (is_array($response) && isset($response['error'])) {
4480 + // Add testing data to error response if available
6920 4481 if ($testing_data !== null) {
6921 4482 $response['testing_data'] = $testing_data;
4483 + //error_log("MxChat Testing: Added testing data to provider error response");
6922 4484 }
6923 - return $response;
4485 + return $response; // Pass through the error with testing data
6924 4486 }
6925 4487
4488 + // For successful non-streaming responses, we don't add testing data here
4489 + // because it will be added in the main handler
6926 4490 return $response;
6927 4491
6928 4492 } catch (Exception $e) {
4493 + //error_log('MXChat Error: ' . $e->getMessage());
6929 4494 $error_response = [
6930 4495 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
6931 4496 'error_code' => 'system_exception',
6932 4497 'exception_details' => $e->getMessage()
@@ -6931,229 +4496,24 @@
6931 4496 'error_code' => 'system_exception',
6932 4497 'exception_details' => $e->getMessage()
6933 4498 ];
6934 4499
4500 + // Add testing data to exception response if available
6935 4501 if ($testing_data !== null) {
6936 4502 $error_response['testing_data'] = $testing_data;
4503 + //error_log("MxChat Testing: Added testing data to exception response");
6937 4504 }
6938 4505
6939 4506 return $error_response;
6940 4507 }
6941 4508 }
6942 -private function mxchat_generate_response_openrouter_stream($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
6943 - try {
6944 - $bot_id = $this->get_current_bot_id($session_id);
6945 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6946 -
6947 - if (!is_array($conversation_history)) {
6948 - $conversation_history = array();
6949 - }
6950 4509
6951 - $formatted_conversation = array();
6952 -
6953 - $formatted_conversation[] = array(
6954 - 'role' => 'system',
6955 - 'content' => $system_prompt_instructions . " " . $relevant_content
6956 - );
6957 -
6958 - foreach ($conversation_history as $message) {
6959 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6960 - $role = $message['role'];
6961 - if ($role === 'bot' || $role === 'agent') {
6962 - $role = 'assistant';
6963 - }
6964 - if (!in_array($role, ['system', 'assistant', 'user'])) {
6965 - $role = 'user';
6966 - }
6967 - $formatted_conversation[] = array(
6968 - 'role' => $role,
6969 - 'content' => $message['content']
6970 - );
6971 - }
6972 - }
6973 -
6974 - if (headers_sent() || !function_exists('curl_init')) {
6975 - $regular_response = $this->mxchat_generate_response_openrouter(
6976 - $selected_model,
6977 - $openrouter_api_key,
6978 - $conversation_history,
6979 - $relevant_content
6980 - );
6981 -
6982 - // Save bot response to transcript
6983 - if (!empty($regular_response) && !empty($session_id)) {
6984 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
6985 - }
6986 -
6987 - $response_data = [
6988 - 'text' => $regular_response,
6989 - 'html' => '',
6990 - 'session_id' => $session_id
6991 - ];
6992 -
6993 - if ($testing_data !== null) {
6994 - $response_data['testing_data'] = $testing_data;
6995 - }
6996 -
6997 - header('Content-Type: application/json');
6998 - echo json_encode($response_data);
6999 - return true;
7000 - }
7001 -
7002 - $body = json_encode([
7003 - 'model' => $selected_model,
7004 - 'messages' => $formatted_conversation,
7005 - 'temperature' => 1,
7006 - 'stream' => true
7007 - ]);
7008 -
7009 - // Setup streaming headers now that we know we're actually streaming
7010 - $this->setup_streaming_headers();
7011 -
7012 - $ch = curl_init();
7013 - curl_setopt($ch, CURLOPT_URL, 'https://openrouter.ai/api/v1/chat/completions');
7014 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7015 - curl_setopt($ch, CURLOPT_POST, true);
7016 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
7017 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7018 - 'Content-Type: application/json',
7019 - 'Authorization: Bearer ' . $openrouter_api_key,
7020 - 'HTTP-Referer: ' . home_url(),
7021 - 'X-Title: ' . get_bloginfo('name')
7022 - ));
7023 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7024 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
7025 -
7026 - $full_response = '';
7027 - $stream_started = false;
7028 - $buffer = '';
7029 -
7030 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
7031 - if (!$stream_started && $testing_data !== null) {
7032 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7033 - flush();
7034 - $stream_started = true;
7035 - }
7036 -
7037 - $buffer .= $data;
7038 - $lines = explode("\n", $buffer);
7039 - $buffer = array_pop($lines);
7040 -
7041 - foreach ($lines as $line) {
7042 - if (trim($line) === '') {
7043 - continue;
7044 - }
7045 -
7046 - if (strpos($line, 'data: ') !== 0) {
7047 - continue;
7048 - }
7049 -
7050 - $json_str = substr($line, 6);
7051 -
7052 - if (trim($json_str) === '[DONE]') {
7053 - echo "data: [DONE]\n\n";
7054 - flush();
7055 - continue;
7056 - }
7057 -
7058 - $json = json_decode(trim($json_str), true);
7059 - if ($json && isset($json['choices'][0]['delta']['content'])) {
7060 - $content = $json['choices'][0]['delta']['content'];
7061 - $full_response .= $content;
7062 -
7063 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
7064 - flush();
7065 - }
7066 - }
7067 -
7068 - return strlen($data);
7069 - });
7070 -
7071 - $response = curl_exec($ch);
7072 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7073 -
7074 - if (curl_errno($ch) || $http_code !== 200) {
7075 - curl_close($ch);
7076 -
7077 - $regular_response = $this->mxchat_generate_response_openrouter(
7078 - $selected_model,
7079 - $openrouter_api_key,
7080 - $conversation_history,
7081 - $relevant_content
7082 - );
7083 -
7084 - $response_data = [
7085 - 'text' => $regular_response,
7086 - 'html' => '',
7087 - 'session_id' => $session_id
7088 - ];
7089 -
7090 - if ($testing_data !== null) {
7091 - $response_data['testing_data'] = $testing_data;
7092 - }
7093 -
7094 - header('Content-Type: application/json');
7095 - echo json_encode($response_data);
7096 - return true;
7097 - }
7098 -
7099 - curl_close($ch);
7100 -
7101 - if (!empty($full_response) && !empty($session_id)) {
7102 - // Prepare RAG context for streaming response
7103 - $rag_context_for_storage = null;
7104 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7105 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7106 -
7107 - if ($has_rag_data || $has_action_data) {
7108 - $rag_context_for_storage = [];
7109 -
7110 - if ($has_rag_data) {
7111 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7112 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7113 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7114 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7115 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7116 - }
7117 -
7118 - if ($has_action_data) {
7119 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7120 - }
7121 - }
7122 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
7123 - }
7124 -
7125 - return true;
7126 -
7127 - } catch (Exception $e) {
7128 - $regular_response = $this->mxchat_generate_response_openrouter(
7129 - $selected_model,
7130 - $openrouter_api_key,
7131 - $conversation_history,
7132 - $relevant_content
7133 - );
7134 -
7135 - $response_data = [
7136 - 'text' => $regular_response,
7137 - 'html' => '',
7138 - 'session_id' => $session_id
7139 - ];
7140 -
7141 - if ($testing_data !== null) {
7142 - $response_data['testing_data'] = $testing_data;
7143 - }
7144 -
7145 - header('Content-Type: application/json');
7146 - echo json_encode($response_data);
7147 - return true;
7148 - }
7149 -}
7150 4510 private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7151 4511 try {
7152 4512 $bot_id = $this->get_current_bot_id($session_id);
7153 4513
7154 4514 // Get system prompt instructions using centralized function
7155 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
4515 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
7156 4516
7157 4517 // Ensure conversation_history is an array
7158 4518 if (!is_array($conversation_history)) {
7159 4519 $conversation_history = array();
@@ -7192,13 +4552,8 @@
7192 4552 $conversation_history,
7193 4553 $relevant_content
7194 4554 );
7195 4555
7196 - // Save bot response to transcript
7197 - if (!empty($regular_response) && !empty($session_id)) {
7198 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7199 - }
7200 -
7201 4556 $response_data = [
7202 4557 'text' => $regular_response,
7203 4558 'html' => '',
7204 4559 'session_id' => $session_id
@@ -7212,47 +4567,16 @@
7212 4567 echo json_encode($response_data);
7213 4568 return true;
7214 4569 }
7215 4570
7216 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
7217 - $is_gpt5_model = (
7218 - strpos($selected_model, 'gpt-5') === 0 ||
7219 - $selected_model === 'gpt-5.2' ||
7220 - $selected_model === 'gpt-5.1-2025-11-13' ||
7221 - $selected_model === 'gpt-5' ||
7222 - $selected_model === 'gpt-5-mini' ||
7223 - $selected_model === 'gpt-5-nano'
7224 - );
7225 -
7226 - // Build request body with optimal settings for fast streaming
7227 - $request_body = [
4571 + // Prepare the request body with stream: true
4572 + $body = json_encode([
7228 4573 'model' => $selected_model,
7229 4574 'messages' => $formatted_conversation,
7230 4575 'temperature' => 1,
7231 4576 'stream' => true
7232 - ];
4577 + ]);
7233 4578
7234 - // Add reasoning_effort only for GPT-5 models that support it
7235 - // These chat models don't support reasoning_effort parameter
7236 - $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');
7237 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
7238 - // GPT-5.1 uses 'low' instead of 'minimal'
7239 - if ($selected_model === 'gpt-5.1-2025-11-13') {
7240 - $request_body['reasoning_effort'] = 'low';
7241 - } elseif ($selected_model === 'gpt-5.5') {
7242 - $request_body['reasoning_effort'] = 'none';
7243 - } elseif ($selected_model === 'gpt-5.4') {
7244 - $request_body['reasoning_effort'] = 'none';
7245 - } else {
7246 - $request_body['reasoning_effort'] = 'minimal';
7247 - }
7248 - }
7249 -
7250 - $body = json_encode($request_body);
7251 -
7252 - // Setup streaming headers now that we know we're actually streaming
7253 - $this->setup_streaming_headers();
7254 -
7255 4579 // Use cURL for streaming support
7256 4580 $ch = curl_init();
7257 4581 curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
7258 4582 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -7325,11 +4649,10 @@
7325 4649 $response = curl_exec($ch);
7326 4650 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7327 4651
7328 4652 if (curl_errno($ch) || $http_code !== 200) {
7329 - $curl_error = curl_error($ch);
7330 4653 curl_close($ch);
7331 -
4654 +
7332 4655 // Fallback to regular response
7333 4656 $regular_response = $this->mxchat_generate_response_openai(
7334 4657 $selected_model,
7335 4658 $api_key,
@@ -7335,34 +4658,19 @@
7335 4658 $api_key,
7336 4659 $conversation_history,
7337 4660 $relevant_content
7338 4661 );
7339 -
7340 - // FIXED: Check if regular response returned an error
7341 - if (is_array($regular_response) && isset($regular_response['error'])) {
7342 - // Send error in SSE format since we're in streaming mode
7343 - echo "data: " . json_encode([
7344 - 'error' => true,
7345 - 'error_message' => $regular_response['error'],
7346 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7347 - 'text' => $regular_response['error'],
7348 - 'message' => $regular_response['error']
7349 - ]) . "\n\n";
7350 - echo "data: [DONE]\n\n";
7351 - flush();
7352 - return true;
7353 - }
7354 -
4662 +
7355 4663 $response_data = [
7356 4664 'text' => $regular_response,
7357 4665 'html' => '',
7358 4666 'session_id' => $session_id
7359 4667 ];
7360 -
4668 +
7361 4669 if ($testing_data !== null) {
7362 4670 $response_data['testing_data'] = $testing_data;
7363 4671 }
7364 -
4672 +
7365 4673 header('Content-Type: application/json');
7366 4674 echo json_encode($response_data);
7367 4675 return true;
7368 4676 }
@@ -7367,36 +4675,16 @@
7367 4675 return true;
7368 4676 }
7369 4677
7370 4678 curl_close($ch);
7371 -
4679 +
7372 4680 // Save the complete response to maintain chat persistence
7373 4681 if (!empty($full_response) && !empty($session_id)) {
7374 - // Prepare RAG context for streaming response
7375 - $rag_context_for_storage = null;
7376 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7377 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7378 -
7379 - if ($has_rag_data || $has_action_data) {
7380 - $rag_context_for_storage = [];
7381 -
7382 - if ($has_rag_data) {
7383 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7384 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7385 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7386 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7387 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7388 - }
7389 -
7390 - if ($has_action_data) {
7391 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7392 - }
7393 - }
7394 - $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);
7395 4683 }
7396 -
4684 +
7397 4685 return true; // Indicate streaming completed successfully
7398 -
4686 +
7399 4687 } catch (Exception $e) {
7400 4688 // Fallback to regular response
7401 4689 $regular_response = $this->mxchat_generate_response_openai(
7402 4690 $selected_model,
@@ -7403,627 +4691,24 @@
7403 4691 $api_key,
7404 4692 $conversation_history,
7405 4693 $relevant_content
7406 4694 );
7407 -
7408 - // FIXED: Check if regular response returned an error
7409 - if (is_array($regular_response) && isset($regular_response['error'])) {
7410 - // Send error in SSE format since we're in streaming mode
7411 - echo "data: " . json_encode([
7412 - 'error' => true,
7413 - 'error_message' => $regular_response['error'],
7414 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7415 - 'text' => $regular_response['error'],
7416 - 'message' => $regular_response['error']
7417 - ]) . "\n\n";
7418 - echo "data: [DONE]\n\n";
7419 - flush();
7420 - return true;
7421 - }
7422 -
4695 +
7423 4696 $response_data = [
7424 4697 'text' => $regular_response,
7425 4698 'html' => '',
7426 4699 'session_id' => $session_id
7427 4700 ];
7428 -
4701 +
7429 4702 if ($testing_data !== null) {
7430 4703 $response_data['testing_data'] = $testing_data;
7431 4704 }
7432 -
4705 +
7433 4706 header('Content-Type: application/json');
7434 4707 echo json_encode($response_data);
7435 4708 return true;
7436 4709 }
7437 4710 }
7438 -
7439 -/**
7440 - * Resolve custom (OpenAI-compatible) provider config from settings.
7441 - * Returns ['base_url','api_key','model','auth_scheme','api_version','chat_url','headers'].
7442 - */
7443 -private function mxchat_resolve_custom_provider() {
7444 - $base_url = isset($this->options['custom_provider_base_url']) ? rtrim(trim((string) $this->options['custom_provider_base_url']), '/') : '';
7445 - $api_key = isset($this->options['custom_provider_api_key']) ? trim((string) $this->options['custom_provider_api_key']) : '';
7446 - $model = isset($this->options['custom_provider_model']) ? trim((string) $this->options['custom_provider_model']) : '';
7447 - $auth_scheme = isset($this->options['custom_provider_auth_scheme']) ? $this->options['custom_provider_auth_scheme'] : 'bearer';
7448 - $api_version = isset($this->options['custom_provider_api_version']) ? trim((string) $this->options['custom_provider_api_version']) : '';
7449 -
7450 - $chat_url = $base_url . '/chat/completions';
7451 - if (!empty($api_version)) {
7452 - $chat_url .= (strpos($chat_url, '?') === false ? '?' : '&') . 'api-version=' . rawurlencode($api_version);
7453 - }
7454 -
7455 - $headers = array('Content-Type: application/json');
7456 - if (!empty($api_key)) {
7457 - if ($auth_scheme === 'api-key') {
7458 - $headers[] = 'api-key: ' . $api_key;
7459 - } else {
7460 - $headers[] = 'Authorization: Bearer ' . $api_key;
7461 - }
7462 - }
7463 -
7464 - return array(
7465 - 'base_url' => $base_url,
7466 - 'api_key' => $api_key,
7467 - 'model' => $model !== '' ? $model : 'default',
7468 - 'auth_scheme' => $auth_scheme,
7469 - 'api_version' => $api_version,
7470 - 'chat_url' => $chat_url,
7471 - 'headers' => $headers,
7472 - );
7473 -}
7474 -
7475 -/**
7476 - * Streaming chat completion against an OpenAI-compatible custom provider
7477 - * (Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI, etc.).
7478 - * Mirrors mxchat_generate_response_openai_stream but with parameterized URL/auth/model.
7479 - */
7480 -private function mxchat_generate_response_custom_stream($selected_model, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7481 - try {
7482 - $cfg = $this->mxchat_resolve_custom_provider();
7483 - if (empty($cfg['base_url'])) {
7484 - return array('error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat'), 'error_code' => 'missing_custom_provider_base_url');
7485 - }
7486 -
7487 - $bot_id = $this->get_current_bot_id($session_id);
7488 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7489 - if (!is_array($conversation_history)) {
7490 - $conversation_history = array();
7491 - }
7492 -
7493 - $formatted_conversation = array();
7494 - $formatted_conversation[] = array(
7495 - 'role' => 'system',
7496 - 'content' => $system_prompt_instructions . ' ' . $relevant_content,
7497 - );
7498 - foreach ($conversation_history as $message) {
7499 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7500 - $role = $message['role'];
7501 - if ($role === 'bot' || $role === 'agent') { $role = 'assistant'; }
7502 - if (!in_array($role, array('system', 'assistant', 'user', 'function', 'tool'))) { $role = 'user'; }
7503 - $formatted_conversation[] = array('role' => $role, 'content' => $message['content']);
7504 - }
7505 - }
7506 -
7507 - if (headers_sent() || !function_exists('curl_init')) {
7508 - // No streaming capability — fall through to non-stream wrapper
7509 - $regular = $this->mxchat_generate_response_custom($selected_model, $conversation_history, $relevant_content);
7510 - if (!empty($regular) && !empty($session_id) && is_string($regular)) {
7511 - $this->mxchat_save_chat_message($session_id, 'bot', $regular);
7512 - }
7513 - $response_data = array('text' => is_string($regular) ? $regular : '', 'html' => '', 'session_id' => $session_id);
7514 - if ($testing_data !== null) { $response_data['testing_data'] = $testing_data; }
7515 - header('Content-Type: application/json');
7516 - echo json_encode($response_data);
7517 - return true;
7518 - }
7519 -
7520 - $request_body = array(
7521 - 'model' => $cfg['model'],
7522 - 'messages' => $formatted_conversation,
7523 - 'stream' => true,
7524 - );
7525 - $body = json_encode($request_body);
7526 -
7527 - $this->setup_streaming_headers();
7528 -
7529 - $ch = curl_init();
7530 - curl_setopt($ch, CURLOPT_URL, $cfg['chat_url']);
7531 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7532 - curl_setopt($ch, CURLOPT_POST, true);
7533 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
7534 - curl_setopt($ch, CURLOPT_HTTPHEADER, $cfg['headers']);
7535 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7536 - curl_setopt($ch, CURLOPT_TIMEOUT, 120);
7537 -
7538 - $full_response = '';
7539 - $stream_started = false;
7540 - $buffer = '';
7541 -
7542 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
7543 - if (!$stream_started && $testing_data !== null) {
7544 - echo "data: " . json_encode(array('testing_data' => $testing_data)) . "\n\n";
7545 - flush();
7546 - $stream_started = true;
7547 - }
7548 - $buffer .= $data;
7549 - $lines = explode("\n", $buffer);
7550 - $buffer = array_pop($lines);
7551 - foreach ($lines as $line) {
7552 - if (trim($line) === '') { continue; }
7553 - if (strpos($line, 'data: ') !== 0) { continue; }
7554 - $json_str = substr($line, 6);
7555 - if (trim($json_str) === '[DONE]') {
7556 - echo "data: [DONE]\n\n";
7557 - flush();
7558 - continue;
7559 - }
7560 - $json = json_decode(trim($json_str), true);
7561 - if ($json && isset($json['choices'][0]['delta']['content'])) {
7562 - $content = $json['choices'][0]['delta']['content'];
7563 - $full_response .= $content;
7564 - echo "data: " . json_encode(array('content' => $content)) . "\n\n";
7565 - flush();
7566 - }
7567 - }
7568 - return strlen($data);
7569 - });
7570 -
7571 - $response = curl_exec($ch);
7572 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7573 -
7574 - if (curl_errno($ch) || $http_code !== 200) {
7575 - $curl_error = curl_error($ch);
7576 - curl_close($ch);
7577 - $regular = $this->mxchat_generate_response_custom($selected_model, $conversation_history, $relevant_content);
7578 - if (is_array($regular) && isset($regular['error'])) {
7579 - echo "data: " . json_encode(array(
7580 - 'error' => true,
7581 - 'error_message' => $regular['error'],
7582 - 'error_code' => $regular['error_code'] ?? 'custom_provider_error',
7583 - 'text' => $regular['error'],
7584 - 'message' => $regular['error'],
7585 - )) . "\n\n";
7586 - echo "data: [DONE]\n\n";
7587 - flush();
7588 - return true;
7589 - }
7590 - $response_data = array('text' => is_string($regular) ? $regular : '', 'html' => '', 'session_id' => $session_id);
7591 - if ($testing_data !== null) { $response_data['testing_data'] = $testing_data; }
7592 - header('Content-Type: application/json');
7593 - echo json_encode($response_data);
7594 - return true;
7595 - }
7596 -
7597 - curl_close($ch);
7598 -
7599 - if (!empty($full_response) && !empty($session_id)) {
7600 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
7601 - }
7602 - return true;
7603 -
7604 - } catch (Exception $e) {
7605 - return array('error' => sprintf(esc_html__('Custom provider error: %s', 'mxchat'), $e->getMessage()), 'error_code' => 'custom_provider_exception');
7606 - }
7607 -}
7608 -
7609 -/**
7610 - * Non-streaming chat completion against a custom OpenAI-compatible provider.
7611 - * Returns string content on success, array['error'=>...] on failure.
7612 - */
7613 -private function mxchat_generate_response_custom($selected_model, $conversation_history, $relevant_content) {
7614 - $cfg = $this->mxchat_resolve_custom_provider();
7615 - if (empty($cfg['base_url'])) {
7616 - return array('error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat'), 'error_code' => 'missing_custom_provider_base_url');
7617 - }
7618 -
7619 - $bot_id = $this->get_current_bot_id(null);
7620 - $system_prompt_instructions = $this->get_system_instructions($bot_id, null);
7621 - if (!is_array($conversation_history)) {
7622 - $conversation_history = array();
7623 - }
7624 -
7625 - $messages = array(array(
7626 - 'role' => 'system',
7627 - 'content' => $system_prompt_instructions . ' ' . $relevant_content,
7628 - ));
7629 - foreach ($conversation_history as $message) {
7630 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7631 - $role = $message['role'];
7632 - if ($role === 'bot' || $role === 'agent') { $role = 'assistant'; }
7633 - if (!in_array($role, array('system', 'assistant', 'user', 'function', 'tool'))) { $role = 'user'; }
7634 - $messages[] = array('role' => $role, 'content' => $message['content']);
7635 - }
7636 - }
7637 -
7638 - $headers_assoc = array('Content-Type' => 'application/json');
7639 - if (!empty($cfg['api_key'])) {
7640 - if ($cfg['auth_scheme'] === 'api-key') {
7641 - $headers_assoc['api-key'] = $cfg['api_key'];
7642 - } else {
7643 - $headers_assoc['Authorization'] = 'Bearer ' . $cfg['api_key'];
7644 - }
7645 - }
7646 -
7647 - $response = wp_remote_post($cfg['chat_url'], array(
7648 - 'headers' => $headers_assoc,
7649 - 'body' => wp_json_encode(array(
7650 - 'model' => $cfg['model'],
7651 - 'messages' => $messages,
7652 - )),
7653 - 'timeout' => 120,
7654 - ));
7655 -
7656 - if (is_wp_error($response)) {
7657 - return array('error' => sprintf(esc_html__('Custom provider request failed: %s', 'mxchat'), $response->get_error_message()), 'error_code' => 'custom_provider_network_error');
7658 - }
7659 - $code = (int) wp_remote_retrieve_response_code($response);
7660 - if ($code < 200 || $code >= 300) {
7661 - return array('error' => sprintf(esc_html__('Custom provider returned HTTP %d.', 'mxchat'), $code), 'error_code' => 'custom_provider_http_error');
7662 - }
7663 - $body = json_decode(wp_remote_retrieve_body($response), true);
7664 - if (isset($body['choices'][0]['message']['content'])) {
7665 - return (string) $body['choices'][0]['message']['content'];
7666 - }
7667 - return array('error' => esc_html__('Custom provider returned an unexpected response shape.', 'mxchat'), 'error_code' => 'custom_provider_response_shape');
7668 -}
7669 -
7670 -/**
7671 - * Generate response using OpenAI Responses API with web search tool
7672 - * This uses the newer Responses API which supports web search functionality
7673 - */
7674 -private function mxchat_generate_response_openai_web_search($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null, $streaming = false) {
7675 - try {
7676 - $bot_id = $this->get_current_bot_id($session_id);
7677 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7678 -
7679 - if (!is_array($conversation_history)) {
7680 - $conversation_history = array();
7681 - }
7682 -
7683 - // Build the input for Responses API
7684 - // The Responses API uses a different format - we need to construct the input properly
7685 - $input_parts = [];
7686 -
7687 - // Add system instructions as context
7688 - $system_context = $system_prompt_instructions . "\n\n" . $relevant_content;
7689 -
7690 - // Build conversation as input items for Responses API
7691 - foreach ($conversation_history as $message) {
7692 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7693 - $role = $message['role'];
7694 - if ($role === 'bot' || $role === 'agent') {
7695 - $role = 'assistant';
7696 - }
7697 - if (!in_array($role, ['assistant', 'user'])) {
7698 - $role = 'user';
7699 - }
7700 - $input_parts[] = [
7701 - 'type' => 'message',
7702 - 'role' => $role,
7703 - 'content' => $message['content']
7704 - ];
7705 - }
7706 - }
7707 -
7708 - // Build request body for Responses API
7709 - $request_body = [
7710 - 'model' => $selected_model,
7711 - 'input' => $input_parts,
7712 - 'instructions' => $system_context,
7713 - 'stream' => $streaming
7714 - ];
7715 -
7716 - // Only add web search tool if web search is enabled in settings
7717 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
7718 - if ($web_search_enabled) {
7719 - $request_body['tools'] = [
7720 - ['type' => 'web_search']
7721 - ];
7722 - }
7723 -
7724 - // Add reasoning effort for supported models
7725 - $is_gpt5_model = strpos($selected_model, 'gpt-5') === 0;
7726 - $no_reasoning_web = array('gpt-5.2', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
7727 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_web, true)) {
7728 - if ($selected_model === 'gpt-5.1-2025-11-13') {
7729 - $request_body['reasoning'] = ['effort' => 'low'];
7730 - } elseif ($selected_model === 'gpt-5.5') {
7731 - $request_body['reasoning'] = ['effort' => 'low'];
7732 - } elseif ($selected_model === 'gpt-5.4') {
7733 - $request_body['reasoning'] = ['effort' => 'low'];
7734 - }
7735 - }
7736 -
7737 - //error_log("MXCHAT WEB SEARCH: Request body: " . json_encode($request_body));
7738 -
7739 - if ($streaming) {
7740 - return $this->mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data);
7741 - } else {
7742 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7743 - }
7744 -
7745 - } catch (Exception $e) {
7746 - //error_log("MXCHAT WEB SEARCH ERROR: " . $e->getMessage());
7747 - return [
7748 - 'error' => sprintf(esc_html__('Web search error: %s', 'mxchat'), esc_html($e->getMessage())),
7749 - 'error_code' => 'web_search_exception'
7750 - ];
7751 - }
7752 -}
7753 -
7754 -/**
7755 - * Handle non-streaming web search response
7756 - */
7757 -private function mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7758 - $request_body['stream'] = false;
7759 -
7760 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
7761 - 'headers' => array(
7762 - 'Authorization' => 'Bearer ' . $api_key,
7763 - 'Content-Type' => 'application/json'
7764 - ),
7765 - 'body' => json_encode($request_body),
7766 - 'timeout' => 90
7767 - ));
7768 -
7769 - if (is_wp_error($response)) {
7770 - //error_log("MXCHAT WEB SEARCH ERROR: WP Error: " . $response->get_error_message());
7771 - return [
7772 - 'error' => esc_html__('Failed to connect to OpenAI web search API', 'mxchat'),
7773 - 'error_code' => 'web_search_connection_error'
7774 - ];
7775 - }
7776 -
7777 - $response_code = wp_remote_retrieve_response_code($response);
7778 - $response_body = wp_remote_retrieve_body($response);
7779 -
7780 - //error_log("MXCHAT WEB SEARCH: Response code: " . $response_code);
7781 - //error_log("MXCHAT WEB SEARCH: Response body (first 2000): " . substr($response_body, 0, 2000));
7782 -
7783 - if ($response_code !== 200) {
7784 - $error_data = json_decode($response_body, true);
7785 - $error_message = $error_data['error']['message'] ?? 'Unknown API error';
7786 - return [
7787 - 'error' => sprintf(esc_html__('OpenAI API error: %s', 'mxchat'), esc_html($error_message)),
7788 - 'error_code' => 'web_search_api_error'
7789 - ];
7790 - }
7791 -
7792 - $result = json_decode($response_body, true);
7793 -
7794 - if (json_last_error() !== JSON_ERROR_NONE) {
7795 - return [
7796 - 'error' => esc_html__('Invalid response from OpenAI', 'mxchat'),
7797 - 'error_code' => 'web_search_json_error'
7798 - ];
7799 - }
7800 -
7801 - // Extract the response text and citations from Responses API format
7802 - $output_text = '';
7803 - $citations = [];
7804 -
7805 - if (isset($result['output'])) {
7806 - foreach ($result['output'] as $output_item) {
7807 - if ($output_item['type'] === 'message' && isset($output_item['content'])) {
7808 - foreach ($output_item['content'] as $content_item) {
7809 - if ($content_item['type'] === 'output_text') {
7810 - $output_text .= $content_item['text'];
7811 -
7812 - // Extract citations/annotations
7813 - if (isset($content_item['annotations'])) {
7814 - foreach ($content_item['annotations'] as $annotation) {
7815 - if ($annotation['type'] === 'url_citation') {
7816 - $citations[] = [
7817 - 'url' => $annotation['url'],
7818 - 'title' => $annotation['title'] ?? ''
7819 - ];
7820 - }
7821 - }
7822 - }
7823 - }
7824 - }
7825 - }
7826 - }
7827 - }
7828 -
7829 - // If we have citations, append them to the response
7830 - if (!empty($citations)) {
7831 - $output_text .= "\n\n**Sources:**\n";
7832 - $seen_urls = [];
7833 - foreach ($citations as $citation) {
7834 - if (!in_array($citation['url'], $seen_urls)) {
7835 - $seen_urls[] = $citation['url'];
7836 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7837 - $output_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7838 - }
7839 - }
7840 - }
7841 -
7842 - // Transcript save is handled by the main handler (mxchat_handle_chat_request)
7843 - // which includes rag_context for the "sources" link in transcripts.
7844 -
7845 - return $output_text;
7846 -}
7847 -
7848 -/**
7849 - * Handle streaming web search response using Responses API
7850 - */
7851 -private function mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7852 - $request_body['stream'] = true;
7853 -
7854 - // Check if we can stream
7855 - if (headers_sent() || !function_exists('curl_init')) {
7856 - // Fallback to non-streaming
7857 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7858 - }
7859 -
7860 - // Setup streaming headers
7861 - $this->setup_streaming_headers();
7862 -
7863 - $ch = curl_init();
7864 - curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/responses');
7865 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7866 - curl_setopt($ch, CURLOPT_POST, true);
7867 - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request_body));
7868 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7869 - 'Content-Type: application/json',
7870 - 'Authorization: Bearer ' . $api_key
7871 - ));
7872 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7873 - curl_setopt($ch, CURLOPT_TIMEOUT, 120);
7874 -
7875 - $full_response = '';
7876 - $stream_started = false;
7877 - $buffer = '';
7878 - $citations = [];
7879 -
7880 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$citations, $testing_data) {
7881 - // Send testing data as first event if available
7882 - if (!$stream_started && $testing_data !== null) {
7883 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7884 - flush();
7885 - $stream_started = true;
7886 - }
7887 -
7888 - $buffer .= $data;
7889 - $lines = explode("\n", $buffer);
7890 - $buffer = array_pop($lines);
7891 -
7892 - foreach ($lines as $line) {
7893 - if (trim($line) === '') continue;
7894 - if (strpos($line, 'data: ') !== 0) continue;
7895 -
7896 - $json_str = substr($line, 6);
7897 -
7898 - if (trim($json_str) === '[DONE]') {
7899 - // Append citations if we have any
7900 - if (!empty($citations)) {
7901 - $citation_text = "\n\n**Sources:**\n";
7902 - $seen_urls = [];
7903 - foreach ($citations as $citation) {
7904 - if (!in_array($citation['url'], $seen_urls)) {
7905 - $seen_urls[] = $citation['url'];
7906 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7907 - $citation_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7908 - }
7909 - }
7910 - echo "data: " . json_encode(['content' => $citation_text]) . "\n\n";
7911 - $full_response .= $citation_text;
7912 - flush();
7913 - }
7914 - echo "data: [DONE]\n\n";
7915 - flush();
7916 - continue;
7917 - }
7918 -
7919 - $json = json_decode(trim($json_str), true);
7920 - if (!$json) continue;
7921 -
7922 - // Handle Responses API streaming events
7923 - // The format is different from Chat Completions
7924 - if (isset($json['type'])) {
7925 - switch ($json['type']) {
7926 - case 'response.output_text.delta':
7927 - // Text content delta
7928 - if (isset($json['delta'])) {
7929 - $content = $json['delta'];
7930 - $full_response .= $content;
7931 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
7932 - flush();
7933 - }
7934 - break;
7935 -
7936 - case 'response.output_item.done':
7937 - // Check for citations in completed items
7938 - if (isset($json['item']['content'])) {
7939 - foreach ($json['item']['content'] as $content_item) {
7940 - if (isset($content_item['annotations'])) {
7941 - foreach ($content_item['annotations'] as $annotation) {
7942 - if ($annotation['type'] === 'url_citation') {
7943 - $citations[] = [
7944 - 'url' => $annotation['url'],
7945 - 'title' => $annotation['title'] ?? ''
7946 - ];
7947 - }
7948 - }
7949 - }
7950 - }
7951 - }
7952 - break;
7953 - }
7954 - }
7955 - }
7956 -
7957 - return strlen($data);
7958 - });
7959 -
7960 - $response = curl_exec($ch);
7961 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7962 -
7963 - if (curl_errno($ch) || $http_code !== 200) {
7964 - $curl_error = curl_error($ch);
7965 - curl_close($ch);
7966 -
7967 - //error_log("MXCHAT WEB SEARCH STREAM ERROR: HTTP $http_code, cURL error: $curl_error");
7968 -
7969 - // Fallback to non-streaming
7970 - $fallback_response = $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7971 -
7972 - if (is_array($fallback_response) && isset($fallback_response['error'])) {
7973 - echo "data: " . json_encode([
7974 - 'error' => true,
7975 - 'error_message' => $fallback_response['error'],
7976 - 'error_code' => $fallback_response['error_code'] ?? 'web_search_error'
7977 - ]) . "\n\n";
7978 - echo "data: [DONE]\n\n";
7979 - flush();
7980 - return true;
7981 - }
7982 -
7983 - $response_data = [
7984 - 'text' => $fallback_response,
7985 - 'html' => '',
7986 - 'session_id' => $session_id
7987 - ];
7988 - if ($testing_data !== null) {
7989 - $response_data['testing_data'] = $testing_data;
7990 - }
7991 - header('Content-Type: application/json');
7992 - echo json_encode($response_data);
7993 - return true;
7994 - }
7995 -
7996 - curl_close($ch);
7997 -
7998 - // Save the complete response with RAG context so the "sources" link
7999 - // appears in transcripts — mirrors the pattern used by Claude/OpenAI streaming.
8000 - if (!empty($full_response) && !empty($session_id)) {
8001 - $rag_context_for_storage = null;
8002 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8003 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8004 -
8005 - if ($has_rag_data || $has_action_data) {
8006 - $rag_context_for_storage = [];
8007 -
8008 - if ($has_rag_data) {
8009 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8010 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8011 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8012 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8013 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8014 - }
8015 -
8016 - if ($has_action_data) {
8017 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8018 - }
8019 - }
8020 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
8021 - }
8022 -
8023 - return true;
8024 -}
8025 -
8026 4711 private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
8027 4712 try {
8028 4713 // Get bot ID from session or request
8029 4714 $bot_id = $this->get_current_bot_id($session_id);
@@ -8028,9 +4713,9 @@
8028 4713 // Get bot ID from session or request
8029 4714 $bot_id = $this->get_current_bot_id($session_id);
8030 4715
8031 4716 // Get system prompt instructions using centralized function
8032 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
4717 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8033 4718 // Ensure conversation_history is an array
8034 4719 if (!is_array($conversation_history)) {
8035 4720 $conversation_history = array();
8036 4721 }
@@ -8082,13 +4767,8 @@
8082 4767 array_slice($conversation_history, 0, -1), // Remove the added content
8083 4768 $relevant_content
8084 4769 );
8085 4770
8086 - // Save bot response to transcript
8087 - if (!empty($regular_response) && !empty($session_id)) {
8088 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
8089 - }
8090 -
8091 4771 // Return as JSON with testing data
8092 4772 $response_data = [
8093 4773 'text' => $regular_response,
8094 4774 'html' => '',
@@ -8107,11 +4787,8 @@
8107 4787 echo json_encode($response_data);
8108 4788 return true; // Indicate we handled the response
8109 4789 }
8110 4790
8111 - // Setup streaming headers now that we know we're actually streaming
8112 - $this->setup_streaming_headers();
8113 -
8114 4791 // Use cURL for streaming support
8115 4792 $ch = curl_init();
8116 4793 curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
8117 4794 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -8216,35 +4893,20 @@
8216 4893 $claude_api_key,
8217 4894 array_slice($conversation_history, 0, -1), // Remove the added content
8218 4895 $relevant_content
8219 4896 );
8220 -
8221 - // FIXED: Check if regular response returned an error
8222 - if (is_array($regular_response) && isset($regular_response['error'])) {
8223 - // Send error in SSE format since we're in streaming mode
8224 - echo "data: " . json_encode([
8225 - 'error' => true,
8226 - 'error_message' => $regular_response['error'],
8227 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
8228 - 'text' => $regular_response['error'],
8229 - 'message' => $regular_response['error']
8230 - ]) . "\n\n";
8231 - echo "data: [DONE]\n\n";
8232 - flush();
8233 - return true;
8234 - }
8235 -
4897 +
8236 4898 $response_data = [
8237 4899 'text' => $regular_response,
8238 4900 'html' => '',
8239 4901 'session_id' => $session_id
8240 4902 ];
8241 -
4903 +
8242 4904 if ($testing_data !== null) {
8243 4905 $response_data['testing_data'] = $testing_data;
8244 4906 //error_log("MxChat Testing: Added testing data to Claude error fallback");
8245 4907 }
8246 -
4908 +
8247 4909 header('Content-Type: application/json');
8248 4910 echo json_encode($response_data);
8249 4911 return true;
8250 4912 }
@@ -8250,29 +4912,9 @@
8250 4912 }
8251 4913
8252 4914 // Save the complete response to maintain chat persistence
8253 4915 if (!empty($full_response) && !empty($session_id)) {
8254 - // Prepare RAG context for streaming response
8255 - $rag_context_for_storage = null;
8256 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8257 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8258 -
8259 - if ($has_rag_data || $has_action_data) {
8260 - $rag_context_for_storage = [];
8261 -
8262 - if ($has_rag_data) {
8263 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8264 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8265 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8266 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8267 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8268 - }
8269 -
8270 - if ($has_action_data) {
8271 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8272 - }
8273 - }
8274 - $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);
8275 4917 }
8276 4918
8277 4919 return true; // Indicate streaming completed successfully
8278 4920
@@ -8277,9 +4919,9 @@
8277 4919 return true; // Indicate streaming completed successfully
8278 4920
8279 4921 } catch (Exception $e) {
8280 4922 //error_log("MxChat Claude streaming exception: " . $e->getMessage());
8281 -
4923 +
8282 4924 // Fallback to regular response on exception
8283 4925 $regular_response = $this->mxchat_generate_response_claude(
8284 4926 $selected_model,
8285 4927 $claude_api_key,
@@ -8285,35 +4927,20 @@
8285 4927 $claude_api_key,
8286 4928 $conversation_history,
8287 4929 $relevant_content
8288 4930 );
8289 -
8290 - // FIXED: Check if regular response returned an error
8291 - if (is_array($regular_response) && isset($regular_response['error'])) {
8292 - // Send error in SSE format since we're in streaming mode
8293 - echo "data: " . json_encode([
8294 - 'error' => true,
8295 - 'error_message' => $regular_response['error'],
8296 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
8297 - 'text' => $regular_response['error'],
8298 - 'message' => $regular_response['error']
8299 - ]) . "\n\n";
8300 - echo "data: [DONE]\n\n";
8301 - flush();
8302 - return true;
8303 - }
8304 -
4931 +
8305 4932 $response_data = [
8306 4933 'text' => $regular_response,
8307 4934 'html' => '',
8308 4935 'session_id' => $session_id
8309 4936 ];
8310 -
4937 +
8311 4938 if ($testing_data !== null) {
8312 4939 $response_data['testing_data'] = $testing_data;
8313 4940 //error_log("MxChat Testing: Added testing data to Claude exception fallback");
8314 4941 }
8315 -
4942 +
8316 4943 header('Content-Type: application/json');
8317 4944 echo json_encode($response_data);
8318 4945 return true;
8319 4946 }
@@ -8323,9 +4950,9 @@
8323 4950 // Get bot ID from session or request
8324 4951 $bot_id = $this->get_current_bot_id($session_id);
8325 4952
8326 4953 // Get system prompt instructions using centralized function
8327 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
4954 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8328 4955
8329 4956 // Ensure conversation_history is an array
8330 4957 if (!is_array($conversation_history)) {
8331 4958 $conversation_history = array();
@@ -8365,13 +4992,8 @@
8365 4992 $conversation_history,
8366 4993 $relevant_content
8367 4994 );
8368 4995
8369 - // Save bot response to transcript
8370 - if (!empty($regular_response) && !empty($session_id)) {
8371 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
8372 - }
8373 -
8374 4996 $response_data = [
8375 4997 'text' => $regular_response,
8376 4998 'html' => '',
8377 4999 'session_id' => $session_id
@@ -8394,11 +5016,8 @@
8394 5016 'temperature' => 0.8,
8395 5017 'stream' => true
8396 5018 ]);
8397 5019
8398 - // Setup streaming headers now that we know we're actually streaming
8399 - $this->setup_streaming_headers();
8400 -
8401 5020 // Use cURL for streaming support
8402 5021 $ch = curl_init();
8403 5022 curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
8404 5023 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -8499,39 +5118,19 @@
8499 5118 return true;
8500 5119 }
8501 5120
8502 5121 curl_close($ch);
8503 -
5122 +
8504 5123 // Save the complete response to maintain chat persistence
8505 5124 if (!empty($full_response) && !empty($session_id)) {
8506 - // Prepare RAG context for streaming response
8507 - $rag_context_for_storage = null;
8508 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8509 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8510 -
8511 - if ($has_rag_data || $has_action_data) {
8512 - $rag_context_for_storage = [];
8513 -
8514 - if ($has_rag_data) {
8515 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8516 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8517 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8518 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8519 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8520 - }
8521 -
8522 - if ($has_action_data) {
8523 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8524 - }
8525 - }
8526 - $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);
8527 5126 }
8528 -
5127 +
8529 5128 return true; // Indicate streaming completed successfully
8530 -
5129 +
8531 5130 } catch (Exception $e) {
8532 5131 //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
8533 -
5132 +
8534 5133 // Fallback to regular response
8535 5134 $regular_response = $this->mxchat_generate_response_xai(
8536 5135 $selected_model,
8537 5136 $xai_api_key,
@@ -8560,9 +5159,9 @@
8560 5159 // Get bot ID from session or request
8561 5160 $bot_id = $this->get_current_bot_id($session_id);
8562 5161
8563 5162 // Get system prompt instructions using centralized function
8564 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5163 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8565 5164
8566 5165 // Ensure conversation_history is an array
8567 5166 if (!is_array($conversation_history)) {
8568 5167 $conversation_history = array();
@@ -8602,13 +5201,8 @@
8602 5201 $conversation_history,
8603 5202 $relevant_content
8604 5203 );
8605 5204
8606 - // Save bot response to transcript
8607 - if (!empty($regular_response) && !empty($session_id)) {
8608 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
8609 - }
8610 -
8611 5205 $response_data = [
8612 5206 'text' => $regular_response,
8613 5207 'html' => '',
8614 5208 'session_id' => $session_id
@@ -8631,11 +5225,8 @@
8631 5225 'temperature' => 0.8,
8632 5226 'stream' => true
8633 5227 ]);
8634 5228
8635 - // Setup streaming headers now that we know we're actually streaming
8636 - $this->setup_streaming_headers();
8637 -
8638 5229 // Use cURL for streaming support
8639 5230 $ch = curl_init();
8640 5231 curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
8641 5232 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -8750,39 +5341,19 @@
8750 5341 return true;
8751 5342 }
8752 5343
8753 5344 curl_close($ch);
8754 -
5345 +
8755 5346 // Save the complete response to maintain chat persistence
8756 5347 if (!empty($full_response) && !empty($session_id)) {
8757 - // Prepare RAG context for streaming response
8758 - $rag_context_for_storage = null;
8759 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8760 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8761 -
8762 - if ($has_rag_data || $has_action_data) {
8763 - $rag_context_for_storage = [];
8764 -
8765 - if ($has_rag_data) {
8766 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8767 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8768 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8769 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8770 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8771 - }
8772 -
8773 - if ($has_action_data) {
8774 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8775 - }
8776 - }
8777 - $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);
8778 5349 }
8779 -
5350 +
8780 5351 return true; // Indicate streaming completed successfully
8781 -
5352 +
8782 5353 } catch (Exception $e) {
8783 5354 //error_log("MxChat DeepSeek streaming exception: " . $e->getMessage());
8784 -
5355 +
8785 5356 // Fallback to regular response
8786 5357 $regular_response = $this->mxchat_generate_response_deepseek(
8787 5358 $selected_model,
8788 5359 $deepseek_api_key,
@@ -8816,112 +5387,8 @@
8816 5387 return true;
8817 5388 }
8818 5389 }
8819 5390
8820 -
8821 -private function mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content) {
8822 - try {
8823 - if (!is_array($conversation_history)) {
8824 - $conversation_history = array();
8825 - }
8826 -
8827 - $bot_id = $this->get_current_bot_id('');
8828 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8829 -
8830 - $formatted_conversation = array();
8831 -
8832 - $formatted_conversation[] = array(
8833 - 'role' => 'system',
8834 - 'content' => $system_prompt_instructions . " " . $relevant_content
8835 - );
8836 -
8837 - foreach ($conversation_history as $message) {
8838 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8839 - $role = $message['role'];
8840 -
8841 - if ($role === 'bot' || $role === 'agent') {
8842 - $role = 'assistant';
8843 - }
8844 - if (!in_array($role, ['system', 'assistant', 'user'])) {
8845 - $role = 'user';
8846 - }
8847 -
8848 - $formatted_conversation[] = array(
8849 - 'role' => $role,
8850 - 'content' => $message['content']
8851 - );
8852 - }
8853 - }
8854 -
8855 - $body = json_encode([
8856 - 'model' => $selected_model,
8857 - 'messages' => $formatted_conversation,
8858 - 'temperature' => 1,
8859 - ]);
8860 -
8861 - $args = [
8862 - 'body' => $body,
8863 - 'headers' => [
8864 - 'Content-Type' => 'application/json',
8865 - 'Authorization' => 'Bearer ' . $openrouter_api_key,
8866 - 'HTTP-Referer' => home_url(),
8867 - 'X-Title' => get_bloginfo('name'),
8868 - ],
8869 - 'timeout' => 60,
8870 - 'redirection' => 5,
8871 - 'blocking' => true,
8872 - 'httpversion' => '1.0',
8873 - 'sslverify' => true,
8874 - ];
8875 -
8876 - $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', $args);
8877 -
8878 - if (is_wp_error($response)) {
8879 - $error_message = $response->get_error_message();
8880 - return [
8881 - 'error' => esc_html__('Connection error when contacting OpenRouter: ', 'mxchat') . esc_html($error_message),
8882 - 'error_code' => 'openrouter_connection_error',
8883 - 'provider' => 'openrouter'
8884 - ];
8885 - }
8886 -
8887 - $status_code = wp_remote_retrieve_response_code($response);
8888 - if ($status_code !== 200) {
8889 - $response_body = wp_remote_retrieve_body($response);
8890 - $decoded_response = json_decode($response_body, true);
8891 -
8892 - $error_message = isset($decoded_response['error']['message'])
8893 - ? $decoded_response['error']['message']
8894 - : 'HTTP Error ' . $status_code;
8895 -
8896 - return [
8897 - 'error' => esc_html__('OpenRouter API error: ', 'mxchat') . esc_html($error_message),
8898 - 'error_code' => 'openrouter_api_error',
8899 - 'provider' => 'openrouter',
8900 - 'status_code' => $status_code
8901 - ];
8902 - }
8903 -
8904 - $response_body = wp_remote_retrieve_body($response);
8905 - $decoded_response = json_decode($response_body, true);
8906 -
8907 - if (isset($decoded_response['choices'][0]['message']['content'])) {
8908 - return trim($decoded_response['choices'][0]['message']['content']);
8909 - } else {
8910 - return [
8911 - 'error' => esc_html__('Unexpected response format from OpenRouter.', 'mxchat'),
8912 - 'error_code' => 'openrouter_response_format_error',
8913 - 'provider' => 'openrouter'
8914 - ];
8915 - }
8916 - } catch (Exception $e) {
8917 - return [
8918 - 'error' => esc_html__('System error when processing OpenRouter request: ', 'mxchat') . esc_html($e->getMessage()),
8919 - 'error_code' => 'openrouter_exception',
8920 - 'provider' => 'openrouter'
8921 - ];
8922 - }
8923 -}
8924 5391 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
8925 5392
8926 5393 // Get bot ID from session or request
8927 5394 $bot_id = $this->get_current_bot_id($session_id);
@@ -8926,9 +5393,9 @@
8926 5393 // Get bot ID from session or request
8927 5394 $bot_id = $this->get_current_bot_id($session_id);
8928 5395
8929 5396 // Get system prompt instructions using centralized function
8930 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5397 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8931 5398
8932 5399 // Clean and validate conversation history
8933 5400 foreach ($conversation_history as &$message) {
8934 5401 // Convert bot and agent roles to assistant
@@ -9032,12 +5499,12 @@
9032 5499 $conversation_history = array();
9033 5500 }
9034 5501
9035 5502 // Get bot ID from session or request
9036 - $bot_id = $this->get_current_bot_id('');
5503 + $bot_id = $this->get_current_bot_id($session_id);
9037 5504
9038 5505 // Get system prompt instructions using centralized function
9039 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5506 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
9040 5507
9041 5508 // Create a new array for the formatted conversation
9042 5509 $formatted_conversation = array();
9043 5510
@@ -9066,44 +5533,15 @@
9066 5533 );
9067 5534 }
9068 5535 }
9069 5536
9070 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
9071 - $is_gpt5_model = (
9072 - strpos($selected_model, 'gpt-5') === 0 ||
9073 - $selected_model === 'gpt-5.2' ||
9074 - $selected_model === 'gpt-5.1-2025-11-13' ||
9075 - $selected_model === 'gpt-5' ||
9076 - $selected_model === 'gpt-5-mini' ||
9077 - $selected_model === 'gpt-5-nano'
9078 - );
9079 -
9080 - // Build request body with optimal settings for fast responses
9081 - $request_body = [
5537 + $body = json_encode([
9082 5538 'model' => $selected_model,
9083 5539 'messages' => $formatted_conversation,
9084 5540 'temperature' => 1,
9085 5541 'stream' => false
9086 - ];
5542 + ]);
9087 5543
9088 - // Add reasoning_effort only for GPT-5 models that support it
9089 - // These chat models don't support reasoning_effort parameter
9090 - $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');
9091 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
9092 - // GPT-5.1 uses 'low' instead of 'minimal'
9093 - if ($selected_model === 'gpt-5.1-2025-11-13') {
9094 - $request_body['reasoning_effort'] = 'low';
9095 - } elseif ($selected_model === 'gpt-5.5') {
9096 - $request_body['reasoning_effort'] = 'none';
9097 - } elseif ($selected_model === 'gpt-5.4') {
9098 - $request_body['reasoning_effort'] = 'none';
9099 - } else {
9100 - $request_body['reasoning_effort'] = 'minimal';
9101 - }
9102 - }
9103 -
9104 - $body = json_encode($request_body);
9105 -
9106 5544 $args = [
9107 5545 'body' => $body,
9108 5546 'headers' => [
9109 5547 'Content-Type' => 'application/json',
@@ -9119,8 +5557,9 @@
9119 5557 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
9120 5558
9121 5559 if (is_wp_error($response)) {
9122 5560 $error_message = $response->get_error_message();
5561 + //error_log('OpenAI API Error: ' . $error_message);
9123 5562 return [
9124 5563 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
9125 5564 'error_code' => 'openai_connection_error',
9126 5565 'provider' => 'openai'
@@ -9139,8 +5578,10 @@
9139 5578 $error_type = isset($decoded_response['error']['type'])
9140 5579 ? $decoded_response['error']['type']
9141 5580 : 'unknown';
9142 5581
5582 + //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
5583 +
9143 5584 // Handle specific error types
9144 5585 switch ($error_type) {
9145 5586 case 'invalid_request_error':
9146 5587 if (strpos($error_message, 'API key') !== false) {
@@ -9188,8 +5629,9 @@
9188 5629
9189 5630 if (isset($decoded_response['choices'][0]['message']['content'])) {
9190 5631 return trim($decoded_response['choices'][0]['message']['content']);
9191 5632 } else {
5633 + //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
9192 5634 return [
9193 5635 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
9194 5636 'error_code' => 'openai_response_format_error',
9195 5637 'provider' => 'openai'
@@ -9195,8 +5637,9 @@
9195 5637 'provider' => 'openai'
9196 5638 ];
9197 5639 }
9198 5640 } catch (Exception $e) {
5641 + //error_log('OpenAI Exception: ' . $e->getMessage());
9199 5642 return [
9200 5643 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
9201 5644 'error_code' => 'openai_exception',
9202 5645 'provider' => 'openai'
@@ -9202,9 +5645,8 @@
9202 5645 'provider' => 'openai'
9203 5646 ];
9204 5647 }
9205 5648 }
9206 -
9207 5649 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
9208 5650 try {
9209 5651 // Get bot ID from session or request
9210 5652 $bot_id = $this->get_current_bot_id($session_id);
@@ -9209,9 +5651,9 @@
9209 5651 // Get bot ID from session or request
9210 5652 $bot_id = $this->get_current_bot_id($session_id);
9211 5653
9212 5654 // Get system prompt instructions using centralized function
9213 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5655 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
9214 5656
9215 5657 // Add system prompt to relevant content
9216 5658 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
9217 5659
@@ -9408,9 +5850,9 @@
9408 5850 // Get bot ID from session or request
9409 5851 $bot_id = $this->get_current_bot_id($session_id);
9410 5852
9411 5853 // Get system prompt instructions using centralized function
9412 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5854 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
9413 5855
9414 5856 // Create a new array for the formatted conversation
9415 5857 $formatted_conversation = array();
9416 5858
@@ -9567,9 +6009,9 @@
9567 6009 // Get bot ID from session or request
9568 6010 $bot_id = $this->get_current_bot_id($session_id);
9569 6011
9570 6012 // Get system prompt instructions using centralized function
9571 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6013 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
9572 6014
9573 6015 // Add system prompt to relevant content
9574 6016 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
9575 6017
@@ -9665,11 +6107,9 @@
9665 6107 ]
9666 6108 ]);
9667 6109
9668 6110 // Prepare the API endpoint
9669 - // Use v1beta for preview models (Gemini 3, experimental), v1 for stable models
9670 - $api_version = (strpos($selected_model, 'preview') !== false || strpos($selected_model, 'exp') !== false) ? 'v1beta' : 'v1';
9671 - $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;
9672 6112
9673 6113 // Set up the API request
9674 6114 $args = [
9675 6115 'body' => $body,
@@ -9711,9 +6151,9 @@
9711 6151
9712 6152
9713 6153 public function test_streaming_request() {
9714 6154 $options = get_option('mxchat_options', []);
9715 - $model = $options['model'] ?? 'gpt-5.1-chat-latest';
6155 + $model = $options['model'] ?? 'gpt-4o';
9716 6156
9717 6157 // Detect provider from model prefix
9718 6158 $provider = strtolower(explode('-', $model)[0]);
9719 6159
@@ -9896,54 +6336,35 @@
9896 6336 }
9897 6337
9898 6338
9899 6339 public function mxchat_enqueue_scripts_styles() {
9900 - // Fetch options from the database first to check loading strategy
9901 - $this->options = get_option('mxchat_options');
9902 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
9903 -
9904 - // Always enqueue CSS immediately
6340 + // Define version numbers for the styles and scripts
6341 + $chat_style_version = '2.4.4';
6342 + $chat_script_version = '2.4.4';
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
9905 6352 wp_enqueue_style(
9906 6353 'mxchat-chat-css',
9907 6354 plugin_dir_url(__FILE__) . '../css/chat-style.css',
9908 6355 array(),
9909 - MXCHAT_VERSION
6356 + $chat_style_version
9910 6357 );
9911 -
9912 - // Handle script loading based on strategy
9913 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
9914 - // Enqueue the script normally
9915 - wp_enqueue_script(
9916 - 'mxchat-chat-js',
9917 - plugin_dir_url(__FILE__) . '../js/chat-script.js',
9918 - array('jquery'),
9919 - MXCHAT_VERSION,
9920 - true
9921 - );
9922 -
9923 - // Add defer attribute if strategy is 'defer'
9924 - if ($loading_strategy === 'defer') {
9925 - wp_script_add_data('mxchat-chat-js', 'strategy', 'defer');
9926 - }
9927 - } else {
9928 - // For delay or interaction-based loading, we'll use a custom loader
9929 - // Don't enqueue the main script - we'll load it dynamically
9930 - add_action('wp_footer', array($this, 'mxchat_output_delayed_script_loader'), 99);
9931 - }
9932 -
6358 + // Fetch options from the database
6359 + $this->options = get_option('mxchat_options');
9933 6360 $prompts_options = get_option('mxchat_prompts_options', array());
9934 -
9935 - // Check if AI theme is active - if so, skip inline colors in JavaScript
9936 - $theme_options = get_option('mxchat_theme_options', array());
9937 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9938 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9939 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9940 -
6361 +
9941 6362 // Prepare settings for JavaScript
9942 6363 $style_settings = array(
9943 6364 'ajax_url' => admin_url('admin-ajax.php'),
9944 6365 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9945 - 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
6366 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
9946 6367 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
9947 6368 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
9948 6369 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
9949 6370 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
@@ -9970,172 +6391,15 @@
9970 6391 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9971 6392 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off', // FIXED
9972 6393 'initial_email_state' => null, // Also fixed this undefined variable
9973 6394 'skip_email_check' => true,
9974 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9975 - 'skip_inline_colors' => $skip_inline_colors,
9976 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array(),
9977 - 'print_button_enabled' => $this->options['print_button_enabled'] ?? 'on',
9978 - 'print_button_label' => esc_html__('Download Transcript', 'mxchat'),
9979 - 'print_header_title' => esc_html(get_bloginfo('name')) . ' — ' . esc_html__('Chat transcript', 'mxchat'),
9980 - 'satisfaction_rating_enabled' => apply_filters(
9981 - 'mxchat_satisfaction_rating_enabled',
9982 - ($this->options['satisfaction_rating_enabled'] ?? 'off') === 'on'
9983 - ),
9984 - 'satisfaction_rating_idle_seconds' => max(5, min(600, intval($this->options['satisfaction_rating_idle_seconds'] ?? 60))),
9985 - 'satisfaction_rating_copy' => array(
9986 - 'question' => !empty($this->options['satisfaction_rating_question']) ? esc_html($this->options['satisfaction_rating_question']) : esc_html__('Was this helpful?', 'mxchat'),
9987 - 'helpful' => esc_html__('Helpful', 'mxchat'),
9988 - 'not_helpful' => esc_html__('Not helpful', 'mxchat'),
9989 - 'dismiss' => esc_html__('Dismiss', 'mxchat'),
9990 - 'thanks' => !empty($this->options['satisfaction_rating_thanks']) ? esc_html($this->options['satisfaction_rating_thanks']) : esc_html__('Thanks! Anything we should improve? (optional)', 'mxchat'),
9991 - 'placeholder' => !empty($this->options['satisfaction_rating_placeholder']) ? esc_html($this->options['satisfaction_rating_placeholder']) : esc_html__('Tell us what could be better…', 'mxchat'),
9992 - 'send' => esc_html__('Send', 'mxchat'),
9993 - 'skip' => esc_html__('Skip', 'mxchat'),
9994 - 'saved' => !empty($this->options['satisfaction_rating_saved']) ? esc_html($this->options['satisfaction_rating_saved']) : esc_html__('Thanks for the feedback.', 'mxchat'),
9995 - ),
6395 + 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
9996 6396 );
9997 -
9998 - // For normal/defer loading, use wp_localize_script
9999 - // For delayed loading, we store settings in a transient to be output inline
10000 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
10001 - wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
10002 - } else {
10003 - // Store settings for the delayed loader to use
10004 - set_transient('mxchat_delayed_settings_' . get_current_user_id(), $style_settings, 60);
10005 - }
6397 + // Pass the settings to the script
6398 + wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
10006 6399 }
10007 6400
10008 -/**
10009 - * Output the delayed script loader for performance optimization
10010 - */
10011 -public function mxchat_output_delayed_script_loader() {
10012 - $this->options = get_option('mxchat_options');
10013 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
10014 - $script_url = plugin_dir_url(__FILE__) . '../js/chat-script.js?ver=' . MXCHAT_VERSION;
10015 6401
10016 - // Get the stored settings
10017 - $prompts_options = get_option('mxchat_prompts_options', array());
10018 - $theme_options = get_option('mxchat_theme_options', array());
10019 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
10020 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
10021 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
10022 -
10023 - $style_settings = array(
10024 - 'ajax_url' => admin_url('admin-ajax.php'),
10025 - 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
10026 - 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
10027 - 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
10028 - 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
10029 - 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
10030 - 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
10031 - 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
10032 - 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
10033 - 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
10034 - 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
10035 - 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
10036 - 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
10037 - 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
10038 - 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
10039 - 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
10040 - 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
10041 - 'icon_color' => $this->options['icon_color'] ?? '#fff',
10042 - 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
10043 - 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
10044 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
10045 - 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
10046 - 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
10047 - 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
10048 - 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
10049 - 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
10050 - 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
10051 - 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
10052 - 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off',
10053 - 'initial_email_state' => null,
10054 - 'skip_email_check' => true,
10055 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
10056 - 'skip_inline_colors' => $skip_inline_colors,
10057 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array(),
10058 - 'print_button_enabled' => $this->options['print_button_enabled'] ?? 'on',
10059 - 'print_button_label' => esc_html__('Download Transcript', 'mxchat'),
10060 - 'print_header_title' => esc_html(get_bloginfo('name')) . ' — ' . esc_html__('Chat transcript', 'mxchat'),
10061 - 'satisfaction_rating_enabled' => apply_filters(
10062 - 'mxchat_satisfaction_rating_enabled',
10063 - ($this->options['satisfaction_rating_enabled'] ?? 'off') === 'on'
10064 - ),
10065 - 'satisfaction_rating_idle_seconds' => max(5, min(600, intval($this->options['satisfaction_rating_idle_seconds'] ?? 60))),
10066 - 'satisfaction_rating_copy' => array(
10067 - 'question' => !empty($this->options['satisfaction_rating_question']) ? esc_html($this->options['satisfaction_rating_question']) : esc_html__('Was this helpful?', 'mxchat'),
10068 - 'helpful' => esc_html__('Helpful', 'mxchat'),
10069 - 'not_helpful' => esc_html__('Not helpful', 'mxchat'),
10070 - 'dismiss' => esc_html__('Dismiss', 'mxchat'),
10071 - 'thanks' => !empty($this->options['satisfaction_rating_thanks']) ? esc_html($this->options['satisfaction_rating_thanks']) : esc_html__('Thanks! Anything we should improve? (optional)', 'mxchat'),
10072 - 'placeholder' => !empty($this->options['satisfaction_rating_placeholder']) ? esc_html($this->options['satisfaction_rating_placeholder']) : esc_html__('Tell us what could be better…', 'mxchat'),
10073 - 'send' => esc_html__('Send', 'mxchat'),
10074 - 'skip' => esc_html__('Skip', 'mxchat'),
10075 - 'saved' => !empty($this->options['satisfaction_rating_saved']) ? esc_html($this->options['satisfaction_rating_saved']) : esc_html__('Thanks for the feedback.', 'mxchat'),
10076 - ),
10077 - );
10078 -
10079 - // Determine delay time based on strategy
10080 - $delay_ms = 0;
10081 - switch ($loading_strategy) {
10082 - case 'delay_1s':
10083 - $delay_ms = 1000;
10084 - break;
10085 - case 'delay_3s':
10086 - $delay_ms = 3000;
10087 - break;
10088 - case 'delay_5s':
10089 - $delay_ms = 5000;
10090 - break;
10091 - }
10092 -
10093 - ?>
10094 - <script type="text/javascript">
10095 - (function() {
10096 - var mxchatLoaded = false;
10097 - var mxchatChat = <?php echo wp_json_encode($style_settings); ?>;
10098 - window.mxchatChat = mxchatChat;
10099 -
10100 - function loadMxChatScript() {
10101 - if (mxchatLoaded) return;
10102 - mxchatLoaded = true;
10103 -
10104 - function appendChatScript() {
10105 - var script = document.createElement('script');
10106 - script.src = <?php echo wp_json_encode($script_url); ?>;
10107 - script.type = 'text/javascript';
10108 - document.body.appendChild(script);
10109 - }
10110 -
10111 - if (typeof jQuery !== 'undefined') {
10112 - appendChatScript();
10113 - } else {
10114 - var jq = document.createElement('script');
10115 - jq.src = <?php echo wp_json_encode(includes_url('js/jquery/jquery.min.js')); ?>;
10116 - jq.onload = appendChatScript;
10117 - document.body.appendChild(jq);
10118 - }
10119 - }
10120 -
10121 - <?php if ($loading_strategy === 'on_interaction'): ?>
10122 - // Load on user interaction
10123 - var events = ['scroll', 'mousemove', 'touchstart', 'keydown', 'click'];
10124 - events.forEach(function(evt) {
10125 - window.addEventListener(evt, loadMxChatScript, {once: true, passive: true});
10126 - });
10127 - // Fallback: load after 8 seconds if no interaction
10128 - setTimeout(loadMxChatScript, 8000);
10129 - <?php else: ?>
10130 - // Load after specified delay
10131 - setTimeout(loadMxChatScript, <?php echo intval($delay_ms); ?>);
10132 - <?php endif; ?>
10133 - })();
10134 - </script>
10135 - <?php
10136 -}
10137 -
10138 6402 /**
10139 6403 * Setup the cron jobs for rate limits with guard against multiple calls
10140 6404 */
10141 6405 public function setup_rate_limit_cron_jobs() {
@@ -10670,11 +6934,8 @@
10670 6934
10671 6935 /**
10672 6936 * AJAX handler to get system information for testing panel
10673 6937 */
10674 -/**
10675 - * AJAX handler to get system information for testing panel
10676 - */
10677 6938 public function mxchat_get_system_info() {
10678 6939 // Verify nonce for security
10679 6940 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10680 6941 wp_send_json_error(['message' => 'Invalid nonce']);
@@ -10691,25 +6952,11 @@
10691 6952 $system_prompt = isset($this->options['system_prompt_instructions'])
10692 6953 ? $this->options['system_prompt_instructions']
10693 6954 : 'No system prompt configured';
10694 6955
10695 - // Get selected model
10696 - $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';
10697 6958
10698 - // Check if OpenRouter is being used
10699 - $is_openrouter = ($selected_model === 'openrouter');
10700 - $openrouter_model = '';
10701 -
10702 - if ($is_openrouter) {
10703 - // Get the actual OpenRouter model that's selected
10704 - $openrouter_model = isset($this->options['openrouter_selected_model'])
10705 - ? $this->options['openrouter_selected_model']
10706 - : 'No OpenRouter model selected';
10707 -
10708 - // Update selected_model display to show both
10709 - $selected_model = 'OpenRouter: ' . $openrouter_model;
10710 - }
10711 -
10712 6959 // Get API key status (just check if they exist, don't expose the keys)
10713 6960 $api_status = [];
10714 6961 $api_status['openai'] = !empty($this->options['api_key']);
10715 6962 $api_status['claude'] = !empty($this->options['claude_api_key']);
@@ -10715,15 +6962,12 @@
10715 6962 $api_status['claude'] = !empty($this->options['claude_api_key']);
10716 6963 $api_status['gemini'] = !empty($this->options['gemini_api_key']);
10717 6964 $api_status['xai'] = !empty($this->options['xai_api_key']);
10718 6965 $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
10719 - $api_status['openrouter'] = !empty($this->options['openrouter_api_key']);
10720 6966
10721 6967 wp_send_json_success([
10722 6968 'system_prompt' => $system_prompt,
10723 6969 'selected_model' => $selected_model,
10724 - 'is_openrouter' => $is_openrouter,
10725 - 'openrouter_model' => $openrouter_model,
10726 6970 'api_status' => $api_status
10727 6971 ]);
10728 6972 }
10729 6973
@@ -10762,42 +7006,24 @@
10762 7006 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10763 7007 wp_send_json_error(['message' => 'Invalid nonce']);
10764 7008 return;
10765 7009 }
10766 -
7010 +
10767 7011 // Only allow admin users
10768 7012 if (!current_user_can('administrator')) {
10769 7013 wp_send_json_error(['message' => 'Unauthorized']);
10770 7014 return;
10771 7015 }
10772 -
10773 - // Check OpenAI Vector Store first (takes priority)
10774 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
10775 - $use_vectorstore = (isset($vectorstore_options['mxchat_use_openai_vectorstore']) && $vectorstore_options['mxchat_use_openai_vectorstore'] === '1');
10776 -
10777 - if ($use_vectorstore) {
10778 - $vectorstore_ids = $vectorstore_options['mxchat_openai_vectorstore_ids'] ?? '';
10779 - $id_count = !empty($vectorstore_ids) ? count(array_filter(array_map('trim', explode(',', $vectorstore_ids)))) : 0;
10780 -
10781 - $kb_info = [
10782 - 'type' => 'OpenAI Vector Store',
10783 - 'status' => 'Active',
10784 - 'documents' => $id_count > 0 ? $id_count . ' vector store' . ($id_count > 1 ? 's' : '') . ' configured' : 'No vector stores configured'
10785 - ];
10786 -
10787 - wp_send_json_success($kb_info);
10788 - return;
10789 - }
10790 -
7016 +
10791 7017 // Check Pinecone vs WordPress
10792 7018 $addon_options = get_option('mxchat_pinecone_addon_options', array());
10793 7019 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
10794 -
7020 +
10795 7021 $kb_info = [
10796 7022 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
10797 7023 'status' => 'Active'
10798 7024 ];
10799 -
7025 +
10800 7026 // Get document count
10801 7027 if ($use_pinecone) {
10802 7028 $kb_info['documents'] = 'Connected to Pinecone';
10803 7029 $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
@@ -10807,9 +7033,9 @@
10807 7033 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
10808 7034 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
10809 7035 $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
10810 7036 }
10811 -
7037 +
10812 7038 wp_send_json_success($kb_info);
10813 7039 }
10814 7040
10815 7041 /**
@@ -10891,13 +7117,9 @@
10891 7117 // Clear any other session-specific transients
10892 7118 delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
10893 7119 delete_transient("mxchat_include_pdf_in_context_{$session_id}");
10894 7120 delete_transient("mxchat_include_word_in_context_{$session_id}");
10895 -
10896 - // Clear form addon state (pending forms and submitted forms)
10897 - delete_option("mxchat_pending_form_{$session_id}");
10898 - delete_option("mxchat_submitted_forms_{$session_id}");
10899 -
7121 +
10900 7122 //error_log("MxChat: Cleared all data for session: {$session_id}");
10901 7123 }
10902 7124
10903 7125 /**
@@ -11072,169 +7294,8 @@
11072 7294 wp_send_json_success(['message' => 'Originating page tracked']);
11073 7295 wp_die();
11074 7296 }
11075 7297
11076 -/**
11077 - * Validate and clean URLs from AI response
11078 - * Removes any URLs that aren't in the knowledge base
11079 - *
11080 - * @param string $response_text The AI-generated response
11081 - * @param array $valid_urls Array of URLs from the knowledge base
11082 - * @return string Cleaned response with invalid URLs removed/flagged
11083 - */
11084 -private function validate_and_clean_urls($response_text, $valid_urls) {
11085 - // DEBUG: Log what we're working with
11086 - //error_log("=== MxChat URL Validation Debug ===");
11087 - //error_log("Valid URLs count: " . count($valid_urls));
11088 - //error_log("Valid URLs: " . print_r($valid_urls, true));
11089 - //error_log("Response text length: " . strlen($response_text));
11090 - //error_log("Response text preview: " . substr($response_text, 0, 500));
11091 -
11092 - // If no valid URLs provided or empty response, return as-is
11093 - if (empty($valid_urls) || empty($response_text)) {
11094 - //error_log("Validation skipped - empty valid_urls or response");
11095 - return $response_text;
11096 - }
11097 -
11098 - // Extract all URLs from the AI response
11099 - // This regex matches http:// and https:// URLs
11100 - preg_match_all(
11101 - '#\bhttps?://[^\s<>"\')\]]+#i',
11102 - $response_text,
11103 - $matches
11104 - );
11105 -
11106 - // If no URLs found in response, return as-is
11107 - if (empty($matches[0])) {
11108 - //error_log("No URLs found in response");
11109 - return $response_text;
11110 - }
11111 -
11112 - $found_urls = $matches[0];
11113 - $cleaned_response = $response_text;
11114 - $removed_count = 0;
11115 -
11116 - // Normalize valid URLs for comparison (remove trailing slashes, fragments, etc.)
11117 - $normalized_valid_urls = array_map(function($url) {
11118 - // Remove trailing slash
11119 - $url = rtrim($url, '/');
11120 - // Remove URL fragments (#section)
11121 - $url = preg_replace('/#.*$/', '', $url);
11122 - // Remove trailing punctuation that might have been captured
11123 - $url = rtrim($url, '.,;:!?');
11124 - return $url;
11125 - }, $valid_urls);
11126 -
11127 - //error_log("Normalized valid URLs: " . print_r($normalized_valid_urls, true));
11128 -
11129 - foreach ($found_urls as $found_url) {
11130 - // Clean up the found URL (remove trailing punctuation that might have been captured)
11131 - $clean_found_url = rtrim($found_url, '.,;:!?)');
11132 -
11133 - // DEBUG: Log each URL being checked
11134 - //error_log("Checking found URL: " . $found_url);
11135 -
11136 - // Normalize for comparison
11137 - $normalized_found = rtrim($clean_found_url, '/');
11138 - $normalized_found = preg_replace('/#.*$/', '', $normalized_found);
11139 -
11140 - //error_log("Normalized found URL: " . $normalized_found);
11141 -
11142 - // Check if this URL exists in our valid URLs list
11143 - $is_valid = false;
11144 -
11145 - //error_log("Starting validation checks for: " . $normalized_found);
11146 -
11147 - // First, try exact match
11148 - if (in_array($normalized_found, $normalized_valid_urls)) {
11149 - $is_valid = true;
11150 - //error_log("EXACT MATCH FOUND");
11151 - } else {
11152 - //error_log("No exact match, checking variations...");
11153 - // If no exact match, check if it's a variation (with query params, etc.)
11154 - foreach ($normalized_valid_urls as $valid_url) {
11155 - //error_log(" Comparing against valid URL: " . $valid_url);
11156 -
11157 - // Check if the found URL starts with a valid URL (handles query params)
11158 - if (strpos($normalized_found, $valid_url) === 0) {
11159 - // Check what comes after the valid URL
11160 - $remainder = substr($normalized_found, strlen($valid_url));
11161 -
11162 - // Only valid if:
11163 - // 1. Exact match (remainder is empty)
11164 - // 2. Query params (starts with ?)
11165 - // 3. Fragment (starts with #)
11166 - if (empty($remainder) || $remainder[0] === '?' || $remainder[0] === '#') {
11167 - $is_valid = true;
11168 - //error_log(" MATCH: Found URL is valid variation of base URL");
11169 - break;
11170 - } else {
11171 - //error_log(" NOT A MATCH: Found URL extends path beyond valid URL (remainder: " . $remainder . ")");
11172 - }
11173 - }
11174 - // Also check the reverse (in case valid URL has query params)
11175 - if (strpos($valid_url, $normalized_found) === 0) {
11176 - $is_valid = true;
11177 - //error_log(" MATCH: Valid URL starts with found URL");
11178 - break;
11179 - }
11180 - }
11181 -
11182 - if (!$is_valid) {
11183 - //error_log("NO MATCH FOUND - URL should be removed");
11184 - }
11185 - }
11186 -
11187 - // If URL is not valid, remove it from the response
11188 - if (!$is_valid) {
11189 - // Log the removal for debugging
11190 - //error_log("MxChat: Removed hallucinated URL: " . $found_url);
11191 - //error_log("MxChat: Valid URLs were: " . implode(', ', array_slice($normalized_valid_urls, 0, 5)));
11192 -
11193 - $removed_count++;
11194 -
11195 - // Check if URL is part of a markdown link: [text](url)
11196 - $markdown_pattern = '/\[([^\]]+)\]\(' . preg_quote($found_url, '/') . '\)/';
11197 - if (preg_match($markdown_pattern, $cleaned_response)) {
11198 - //error_log("Found markdown link, removing but keeping text");
11199 - // Remove the markdown link but keep the text
11200 - $cleaned_response = preg_replace($markdown_pattern, '$1', $cleaned_response);
11201 - }
11202 - // Check if URL is part of an HTML link: <a href="url">text</a>
11203 - else if (preg_match('/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>(.*?)<\/a>/i', $cleaned_response, $link_match)) {
11204 - //error_log("Found HTML link, removing but keeping text");
11205 - // Remove the HTML link but keep the text
11206 - $link_text = $link_match[1];
11207 - $cleaned_response = preg_replace(
11208 - '/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>.*?<\/a>/i',
11209 - $link_text,
11210 - $cleaned_response
11211 - );
11212 - }
11213 - // Otherwise just remove the bare URL
11214 - else {
11215 - //error_log("Removing bare URL");
11216 - $cleaned_response = str_replace($found_url, '', $cleaned_response);
11217 - }
11218 - }
11219 - }
11220 -
11221 - // Log summary if any URLs were removed
11222 - if ($removed_count > 0) {
11223 - //error_log("MxChat: URL Validation Summary - Removed {$removed_count} hallucinated URL(s)");
11224 - } else {
11225 - //error_log("MxChat: URL Validation Summary - No URLs removed, all were valid");
11226 - }
11227 -
11228 - // Clean up any double spaces or awkward punctuation left behind
11229 - // IMPORTANT: Only collapse horizontal whitespace (spaces/tabs), preserve newlines for markdown formatting
11230 - $cleaned_response = preg_replace('/[^\S\n]+/', ' ', $cleaned_response); // Collapse spaces/tabs but NOT newlines
11231 - $cleaned_response = preg_replace('/[^\S\n]+([.,;:!?])/', '$1', $cleaned_response); // Same for punctuation cleanup
11232 -
11233 - //error_log("Final cleaned response: " . $cleaned_response);
11234 -
11235 - return trim($cleaned_response);
11236 -}
11237 7298
11238 7299 /**
11239 7300 * AJAX handler to get current chat mode for a session
11240 7301 */