PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.5.8
MxChat – AI Chatbot & Content Generation for WordPress v2.5.8
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 +607 -2939 3.2.32.5.8 View file →
@@ -11,40 +11,11 @@
11 11 private $productCardHtml;
12 12 private $word_handler;
13 13 private $last_similarity_analysis = null;
14 14 private $current_valid_urls = [];
15 - private $last_vectorstore_error = null;
16 15 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 16
19 17 /**
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 -
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 -/**
47 18 * Class constructor
48 19 */
49 20 public function __construct() {
50 21 $this->options = get_option('mxchat_options');
@@ -111,28 +82,16 @@
111 82 // Add chat mode checking actions
112 83 add_action('wp_ajax_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
113 84 add_action('wp_ajax_nopriv_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
114 85
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 86 // Auto-email transcript action
120 87 add_action('mxchat_send_delayed_transcript', array($this, 'mxchat_send_delayed_transcript'), 10, 1);
121 -
88 +
122 89 add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
123 90
124 91
125 92 }
126 93
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 94 // In your core plugin's check_actions_for_addons method:
136 95 public function check_actions_for_addons($default, $message, $user_id, $session_id) {
137 96 //error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
138 97
@@ -157,17 +116,21 @@
157 116
158 117 $session_id = sanitize_text_field($_POST['session_id']);
159 118
160 119 // SECURITY FIX: Verify session ownership before retrieving data
161 - // If IP/user changed, signal frontend to reset session instead of blocking
162 120 $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
163 -
121 +
164 122 // Check if this session has an owner recorded
165 123 $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) {
124 +
125 + // If session has an owner and it doesn't match current user, deny access
126 + if ($session_owner && $session_owner !== $current_user_identifier) {
127 + wp_send_json_error(['message' => esc_html__('Unauthorized access.', 'mxchat')]);
128 + wp_die();
129 + }
130 +
131 + // If no owner is set yet, claim ownership (for legacy sessions)
132 + if (!$session_owner) {
170 133 update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
171 134 }
172 135
173 136 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
@@ -187,25 +150,10 @@
187 150 'chat_mode' => $chat_mode
188 151 ]);
189 152 wp_die();
190 153 }
191 -private function mxchat_fetch_conversation_history_for_ai($session_id, $session_start_timestamp = 0) {
154 +private function mxchat_fetch_conversation_history_for_ai($session_id) {
192 155 $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 156 $formatted_history = [];
209 157
210 158 // Adjusted for code-heavy conversations
211 159 $max_tokens = 120000; // Context window size
@@ -303,15 +251,8 @@
303 251 'callback' => [$this, 'handle_slack_messages'],
304 252 'permission_callback' => [$this, 'verify_slack_request'],
305 253 ]);
306 254
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 255 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
315 256 }
316 257
317 258 /**
@@ -351,11 +292,10 @@
351 292 //error_log(esc_html__('Slack request timestamp too old', 'mxchat'));
352 293 return false;
353 294 }
354 295
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();
296 + // Get raw request body
297 + $request_body = file_get_contents('php://input');
358 298
359 299 // Create the signature base string
360 300 $sig_basestring = "v0:{$timestamp}:{$request_body}";
361 301
@@ -364,43 +304,8 @@
364 304
365 305 // Compare signatures
366 306 return hash_equals($my_signature, $slack_signature);
367 307 }
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 308 public function mxchat_stream_events(WP_REST_Request $request) {
404 309 header('Content-Type: text/event-stream');
405 310 header('Cache-Control: no-cache');
406 311 header('Connection: keep-alive');
@@ -434,9 +339,9 @@
434 339
435 340
436 341
437 342
438 -private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null, $rag_context = null) {
343 +private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null) {
439 344 global $wpdb;
440 345 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
441 346 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
442 347
@@ -610,17 +515,9 @@
610 515 $insert_data['originating_page_title'] = $stored_originating['title'] ?? '';
611 516 }
612 517 }
613 518 }
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 -
519 +
623 520 $wpdb->insert($table_name, $insert_data);
624 521 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
625 522
626 523 // 9) Send notification email if this is the first user message in a new session
@@ -833,17 +730,12 @@
833 730 $transcript_content .= "[{$msg->timestamp}] {$role_label}:\n";
834 731 $transcript_content .= $msg->message . "\n\n";
835 732 }
836 733
837 - // Create temporary file for attachment using WP_Filesystem
734 + // Create temporary file for attachment
838 735 $upload_dir = wp_upload_dir();
839 736 $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);
737 + file_put_contents($temp_file, $transcript_content);
846 738
847 739 // Prepare email
848 740 $subject = sprintf('[%s] Chat Transcript - Session %s', get_bloginfo('name'), substr($session_id, 0, 8));
849 741
@@ -874,10 +766,8 @@
874 766 public function mxchat_handle_save_email_and_response() {
875 767 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
876 768 //error_log('DEBUG: POST data: ' . print_r($_POST, true));
877 769
878 - nocache_headers();
879 -
880 770 // Validate nonce
881 771 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
882 772 //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
883 773 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
@@ -889,9 +779,9 @@
889 779 $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
890 780
891 781 //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}, name: {$name}");
892 782
893 - if (empty($session_id) || $session_id === 'null' || empty($email)) {
783 + if (empty($session_id) || empty($email)) {
894 784 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
895 785 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
896 786 wp_die();
897 787 }
@@ -908,15 +798,15 @@
908 798 }
909 799
910 800 // 1) Always store email in wp_options
911 801 $email_option_key = "mxchat_email_{$session_id}";
912 - update_option($email_option_key, $email, 'no');
802 + update_option($email_option_key, $email);
913 803 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$email_option_key} => {$email}");
914 804
915 805 // Store name in wp_options if provided
916 806 if (!empty($name)) {
917 807 $name_option_key = "mxchat_name_{$session_id}";
918 - update_option($name_option_key, $name, 'no');
808 + update_option($name_option_key, $name);
919 809 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$name_option_key} => {$name}");
920 810 }
921 811
922 812 // 2) (Optional) Also store in DB if a row already exists
@@ -960,10 +850,8 @@
960 850
961 851 public function mxchat_check_email_provided() {
962 852 //error_log('[DEBUG] ---------- mxchat_check_email_provided START ----------');
963 853
964 - nocache_headers();
965 -
966 854 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
967 855 //error_log('[ERROR] Invalid nonce in mxchat_check_email_provided');
968 856 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
969 857 }
@@ -968,9 +856,9 @@
968 856 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
969 857 }
970 858
971 859 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
972 - if (empty($session_id) || $session_id === 'null') {
860 + if (empty($session_id)) {
973 861 //error_log('[ERROR] No session ID provided in mxchat_check_email_provided');
974 862 wp_send_json_error(['message' => esc_html__('No session ID provided', 'mxchat')]);
975 863 }
976 864
@@ -1059,10 +947,10 @@
1059 947 global $wpdb;
1060 948
1061 949 // Debug: Log incoming bot_id
1062 950 $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);
951 + error_log("=== MXCHAT DEBUG: Starting chat request ===");
952 + error_log("MXCHAT DEBUG: Bot ID received: " . $bot_id);
1065 953
1066 954 // Get bot-specific options
1067 955 $bot_options = $this->get_bot_options($bot_id);
1068 956 $current_options = !empty($bot_options) ? $bot_options : $this->options;
@@ -1067,18 +955,31 @@
1067 955 $bot_options = $this->get_bot_options($bot_id);
1068 956 $current_options = !empty($bot_options) ? $bot_options : $this->options;
1069 957
1070 958 // 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 959 $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'));
960 + isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on';
1075 961
1076 962 // ADDED: Store streaming state in class property for use in private methods
1077 963 $this->is_streaming = $is_streaming;
1078 964
1079 - // NOTE: Streaming headers are now set later via setup_streaming_headers()
1080 - // This allows actions/forms to return JSON responses without header conflicts
965 + // Set streaming headers if needed
966 + if ($is_streaming) {
967 + // Disable output buffering
968 + while (ob_get_level()) {
969 + ob_end_flush(); // Changed from ob_end_clean()
970 + }
971 +
972 + // Set headers for SSE
973 + header('Content-Type: text/event-stream');
974 + header('Cache-Control: no-cache');
975 + header('Connection: keep-alive');
976 + header('X-Accel-Buffering: no');
977 +
978 + // Add these new lines:
979 + ob_implicit_flush(true);
980 + flush();
981 + }
1081 982
1082 983 // Check if MX Chat Moderation is active
1083 984 if (class_exists('MX_Chat_Moderation')) {
1084 985 // Get user email and IP
@@ -1144,29 +1045,20 @@
1144 1045
1145 1046 // Rest of your existing code...
1146 1047 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
1147 1048
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 1049 if (empty($session_id)) {
1158 1050 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
1159 1051 wp_die();
1160 1052 }
1161 1053
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
1054 + // SECURITY FIX: Verify session ownership before processing chat request
1164 1055 $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
1165 1056 $session_owner = get_option("mxchat_session_owner_{$session_id}");
1166 1057
1167 - if (!$session_owner || $session_owner !== $current_user_identifier) {
1168 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
1058 + if ($session_owner && $session_owner !== $current_user_identifier) {
1059 + wp_send_json_error(esc_html__('Unauthorized access.', 'mxchat'));
1060 + wp_die();
1169 1061 }
1170 1062
1171 1063 // Validate and sanitize the incoming message
1172 1064 if (empty($_POST['message'])) {
@@ -1573,37 +1465,54 @@
1573 1465
1574 1466 if ($testing_data !== null) {
1575 1467 $response_data['testing_data'] = $testing_data;
1576 1468 }
1577 -
1469 +
1470 + // Clear streaming headers if they were set
1471 + if ($is_streaming) {
1472 + header_remove('Content-Type');
1473 + header_remove('Cache-Control');
1474 + header_remove('Connection');
1475 + header_remove('X-Accel-Buffering');
1476 + header('Content-Type: application/json');
1477 + }
1478 +
1578 1479 wp_send_json($response_data);
1579 1480 wp_die();
1580 1481 } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1581 1482 // Intent returned true and set fallbackResponse
1582 -
1583 - // SAVE TO TRANSCRIPT
1483 +
1484 + // SAVE TO TRANSCRIPT FIRST
1584 1485 if (!empty($this->fallbackResponse['text'])) {
1585 1486 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1586 1487 }
1587 - // Save action HTML (product cards, featured products, etc.) so it renders in transcripts
1588 1488 if (!empty($this->fallbackResponse['html'])) {
1589 1489 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1590 1490 }
1591 -
1491 +
1592 1492 $response_data = [
1593 1493 'text' => $this->fallbackResponse['text'] ?? '',
1594 1494 'html' => $this->fallbackResponse['html'] ?? '',
1595 1495 'session_id' => $session_id
1596 1496 ];
1597 -
1497 +
1598 1498 if (isset($this->fallbackResponse['chat_mode'])) {
1599 1499 $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
1600 1500 }
1601 -
1501 +
1602 1502 if ($testing_data !== null) {
1603 1503 $response_data['testing_data'] = $testing_data;
1604 1504 }
1605 -
1505 +
1506 + // Clear streaming headers if they were set
1507 + if ($is_streaming) {
1508 + header_remove('Content-Type');
1509 + header_remove('Cache-Control');
1510 + header_remove('Connection');
1511 + header_remove('X-Accel-Buffering');
1512 + header('Content-Type: application/json');
1513 + }
1514 +
1606 1515 wp_send_json($response_data);
1607 1516 wp_die();
1608 1517 }
1609 1518 }
@@ -1608,13 +1517,11 @@
1608 1517 }
1609 1518 }
1610 1519
1611 1520 // If we get here, no intent matched OR the intent didn't provide a usable response
1612 -
1521 +
1613 1522 // 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);
1523 + $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
1617 1524 $this->mxchat_increment_chat_count();
1618 1525
1619 1526 // Generate embedding for the user's query - USE BOT-SPECIFIC API KEY
1620 1527 $api_key = $current_options['api_key'] ?? $this->options['api_key'];
@@ -1692,34 +1599,30 @@
1692 1599 $context_content .= "Page Content: " . $page_context['content'] . "\n";
1693 1600 $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1694 1601 }
1695 1602
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);
1603 + // Get relevant content from knowledge base - PASS BOT_ID
1604 + $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id);
1698 1605
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;
1703 -
1704 - $system_instructions = $this->get_system_instructions($bot_id, $session_id);
1705 - if ($citation_links_enabled && !empty($system_instructions)) {
1606 + // NEW: Also extract URLs from system instructions
1607 + $system_instructions = $this->get_system_instructions($bot_id);
1608 + if (!empty($system_instructions)) {
1706 1609 preg_match_all(
1707 - '#\bhttps?://[^\s<>"\']+#i',
1708 - $system_instructions,
1610 + '#\bhttps?://[^\s<>"\']+#i',
1611 + $system_instructions,
1709 1612 $system_instruction_urls
1710 1613 );
1711 -
1614 +
1712 1615 if (!empty($system_instruction_urls[0])) {
1713 1616 // Merge with existing valid URLs
1714 1617 $this->current_valid_urls = array_merge(
1715 - $this->current_valid_urls,
1618 + $this->current_valid_urls,
1716 1619 $system_instruction_urls[0]
1717 1620 );
1718 1621 // Remove duplicates
1719 1622 $this->current_valid_urls = array_unique($this->current_valid_urls);
1720 -
1721 - //error_log("Added " . count($system_instruction_urls[0]) . " URLs from system instructions");
1623 +
1624 + error_log("Added " . count($system_instruction_urls[0]) . " URLs from system instructions");
1722 1625 }
1723 1626 }
1724 1627
1725 1628 // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
@@ -1727,10 +1630,8 @@
1727 1630 // Update testing data with the REAL similarity analysis
1728 1631 $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1729 1632 $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1730 1633 $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 1634 }
1734 1635 // ===== END SIMILARITY DATA CAPTURE =====
1735 1636
1736 1637 // NEW: Add valid URLs to testing data for admin panel display (AFTER similarity data)
@@ -1735,9 +1636,9 @@
1735 1636
1736 1637 // NEW: Add valid URLs to testing data for admin panel display (AFTER similarity data)
1737 1638 if ($testing_data !== null && !empty($this->current_valid_urls)) {
1738 1639 $testing_data['approved_urls'] = array_values($this->current_valid_urls);
1739 - //error_log("Added " . count($this->current_valid_urls) . " approved URLs to testing data");
1640 + error_log("Added " . count($this->current_valid_urls) . " approved URLs to testing data");
1740 1641 }
1741 1642
1742 1643 if (!empty($relevant_content)) {
1743 1644 $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
@@ -1744,10 +1645,10 @@
1744 1645 } else {
1745 1646 $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1746 1647 }
1747 1648
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)) {
1649 + // NEW: Add approved URLs list to context for AI
1650 + if (!empty($this->current_valid_urls)) {
1750 1651 $context_content .= "===== APPROVED URLS FOR CITATIONS =====\n";
1751 1652 $context_content .= "You may ONLY use these exact URLs in your response:\n";
1752 1653 foreach ($this->current_valid_urls as $url) {
1753 1654 $context_content .= "- " . $url . "\n";
@@ -1788,9 +1689,9 @@
1788 1689
1789 1690 $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1790 1691
1791 1692 // 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';
1693 + $selected_model = isset($current_options['model']) ? $current_options['model'] : 'gpt-4o';
1793 1694
1794 1695 $response = $this->mxchat_generate_response(
1795 1696 $context_content,
1796 1697 $current_options['api_key'] ?? $this->options['api_key'],
@@ -1840,50 +1741,25 @@
1840 1741 wp_die();
1841 1742 }
1842 1743
1843 1744 // 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));
1745 + error_log("=== BEFORE URL VALIDATION ===");
1746 + error_log("current_valid_urls is empty? " . (empty($this->current_valid_urls) ? 'YES' : 'NO'));
1747 + error_log("current_valid_urls count: " . count($this->current_valid_urls));
1748 + error_log("current_valid_urls content: " . print_r($this->current_valid_urls, true));
1848 1749
1849 1750 // If we get here, the response is valid text - now validate URLs
1850 1751 if (!empty($this->current_valid_urls)) {
1851 - //error_log("CALLING validate_and_clean_urls");
1752 + error_log("CALLING validate_and_clean_urls");
1852 1753 $response = $this->validate_and_clean_urls($response, $this->current_valid_urls);
1853 1754 } else {
1854 - //error_log("SKIPPING validation - current_valid_urls is empty");
1755 + error_log("SKIPPING validation - current_valid_urls is empty");
1855 1756 }
1856 1757 // ===== END URL VALIDATION =====
1758 +
1759 + // Save the cleaned response
1760 + $this->mxchat_save_chat_message($session_id, 'bot', $response);
1857 1761
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 1762 // Step 5: Save additional content if available
1887 1763 if (!empty($this->productCardHtml)) {
1888 1764 $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1889 1765 }
@@ -1892,13 +1768,8 @@
1892 1768 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1893 1769 }
1894 1770
1895 1771 // 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 1772 $response_data = [
1902 1773 'text' => $response,
1903 1774 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1904 1775 'session_id' => $session_id
@@ -1903,18 +1774,8 @@
1903 1774 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1904 1775 'session_id' => $session_id
1905 1776 ];
1906 1777
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 1778 // Always add testing data for admins (no toggle needed)
1918 1779 if ($testing_data !== null) {
1919 1780 $response_data['testing_data'] = $testing_data;
1920 1781 }
@@ -1928,12 +1789,12 @@
1928 1789 * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1929 1790 */
1930 1791 // Also debug the bot options retrieval
1931 1792 private function get_bot_options($bot_id = 'default') {
1932 - //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
1793 + error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
1933 1794
1934 1795 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')");
1796 + error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
1936 1797 return array();
1937 1798 }
1938 1799
1939 1800 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
@@ -1938,11 +1799,11 @@
1938 1799
1939 1800 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
1940 1801
1941 1802 if (!empty($bot_options)) {
1942 - //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1803 + error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1943 1804 if (isset($bot_options['similarity_threshold'])) {
1944 - //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1805 + error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1945 1806 }
1946 1807 }
1947 1808
1948 1809 return is_array($bot_options) ? $bot_options : array();
@@ -1953,13 +1814,13 @@
1953 1814 * Used in the knowledge retrieval functions
1954 1815 */
1955 1816 // Also add debugging to your get_bot_pinecone_config function
1956 1817 private function get_bot_pinecone_config($bot_id = 'default') {
1957 - //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1818 + error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1958 1819
1959 1820 // If default bot or multi-bot add-on not active, use default Pinecone config
1960 1821 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')");
1822 + error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1962 1823 $addon_options = get_option('mxchat_pinecone_addon_options', array());
1963 1824 $config = array(
1964 1825 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
1965 1826 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
@@ -1965,24 +1826,24 @@
1965 1826 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
1966 1827 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
1967 1828 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
1968 1829 );
1969 - //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1830 + error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1970 1831 return $config;
1971 1832 }
1972 1833
1973 - //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1834 + error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1974 1835
1975 1836 // Hook for multi-bot add-on to provide bot-specific Pinecone config
1976 1837 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1977 1838
1978 1839 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'));
1840 + error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1841 + error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1842 + error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1843 + error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1983 1844 } else {
1984 - //error_log("MXCHAT DEBUG: Filter returned empty config!");
1845 + error_log("MXCHAT DEBUG: Filter returned empty config!");
1985 1846 }
1986 1847
1987 1848 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1988 1849 }
@@ -2058,29 +1919,19 @@
2058 1919 $intents = $wpdb->get_results($query);
2059 1920 } else {
2060 1921 $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
2061 1922 }
2062 -
1923 +
2063 1924 if (empty($intents)) {
2064 1925 return false;
2065 1926 }
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 -
1927 +
2077 1928 $highest_similarity = -INF;
2078 1929 $matched_intent = null;
2079 -
1930 +
2080 1931 // Array to store action analysis for testing panel
2081 1932 $action_analysis = [];
2082 -
1933 +
2083 1934 foreach ($intents as $intent) {
2084 1935 // Additional check for enabled state
2085 1936 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2086 1937 if (!$is_enabled) {
@@ -2085,56 +1936,26 @@
2085 1936 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2086 1937 if (!$is_enabled) {
2087 1938 continue;
2088 1939 }
2089 -
1940 +
2090 1941 // Check if this action is enabled for the current bot
2091 1942 if (!$this->is_action_enabled_for_bot($intent, $current_bot_id)) {
2092 1943 continue;
2093 1944 }
2094 -
2095 - $best_similarity = -INF;
2096 - $matched_phrase_text = '';
2097 -
2098 - // Check legacy embedding vector (existing behavior)
1945 +
2099 1946 $intent_embedding_serialized = $intent->embedding_vector;
2100 1947 $intent_embedding = $intent_embedding_serialized
2101 1948 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
2102 1949 : 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) {
1950 +
1951 + if (!is_array($intent_embedding)) {
2131 1952 continue;
2132 1953 }
2133 -
2134 - $similarity = $best_similarity;
1954 +
1955 + $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2135 1956 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
2136 -
1957 +
2137 1958 // Store action analysis data for testing panel
2138 1959 $action_analysis[] = [
2139 1960 'intent_label' => $intent->intent_label,
2140 1961 'callback_function' => $intent->callback_function,
@@ -2142,12 +1963,11 @@
2142 1963 'similarity_percentage' => round($similarity * 100, 2),
2143 1964 'threshold' => $intent_threshold,
2144 1965 'threshold_percentage' => round($intent_threshold * 100, 2),
2145 1966 'above_threshold' => $similarity >= $intent_threshold,
2146 - 'matched_phrase' => $matched_phrase_text,
2147 1967 'triggered' => false // Will be updated below if this intent is triggered
2148 1968 ];
2149 -
1969 +
2150 1970 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
2151 1971 $highest_similarity = $similarity;
2152 1972 $matched_intent = $intent;
2153 1973 }
@@ -2218,22 +2038,16 @@
2218 2038 // If enabled_bots column doesn't exist or is null, assume it's enabled for all bots (backward compatibility)
2219 2039 if (!isset($intent->enabled_bots) || empty($intent->enabled_bots)) {
2220 2040 return true;
2221 2041 }
2222 -
2042 +
2223 2043 $enabled_bots = json_decode($intent->enabled_bots, true);
2224 -
2044 +
2225 2045 // If JSON decode fails or returns empty array, assume enabled for all (backward compatibility)
2226 2046 if (!is_array($enabled_bots) || empty($enabled_bots)) {
2227 2047 return true;
2228 2048 }
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 -
2049 +
2236 2050 // Check if the current bot is in the enabled bots list
2237 2051 return in_array($bot_id, $enabled_bots);
2238 2052 }
2239 2053
@@ -2272,17 +2086,17 @@
2272 2086
2273 2087 public function mxchat_generate_image($message, $user_id, $session_id) {
2274 2088 //error_log("Starting image generation for message: " . $message);
2275 2089
2276 - // Prepare a prompt for OpenAI image generation
2090 + // Prepare a prompt for DALL-E
2277 2091 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2278 -
2092 +
2279 2093 // Use the existing OpenAI API key
2280 2094 $openai_api_key = sanitize_text_field($this->options['api_key']);
2281 -
2282 - // Call OpenAI GPT Image to generate an image
2283 - $image_response = $this->mxchat_generate_openai_image($prompt, $openai_api_key);
2284 2095
2096 + // Call DALL-E to generate an image
2097 + $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
2098 +
2285 2099 // Check if the response contains an image URL
2286 2100 if (isset($image_response['imageUrl'])) {
2287 2101 $image_url = esc_url_raw($image_response['imageUrl']);
2288 2102
@@ -2325,103 +2139,24 @@
2325 2139 // Return the response directly instead of relying on the property
2326 2140 return $this->fallbackResponse;
2327 2141 }
2328 2142 }
2329 -
2330 -public function mxchat_generate_gemini_image($message, $user_id, $session_id) {
2331 - $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2332 -
2333 - $gemini_api_key = sanitize_text_field($this->options['gemini_api_key'] ?? '');
2334 - if (empty($gemini_api_key)) {
2335 - $response_text = esc_html__("Gemini API key is not configured.", 'mxchat');
2336 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2337 - return ['text' => $response_text, 'html' => '', 'images' => []];
2338 - }
2339 -
2340 - $image_response = $this->mxchat_generate_imagen_image($prompt, $gemini_api_key);
2341 -
2342 - if (isset($image_response['imageUrl'])) {
2343 - $image_url = esc_url_raw($image_response['imageUrl']);
2344 -
2345 - $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
2346 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
2347 -
2348 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2349 - $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
2350 -
2351 - $this->fallbackResponse = [
2352 - 'text' => $response_text,
2353 - 'html' => $response_html,
2354 - 'images' => [$image_url]
2355 - ];
2356 -
2357 - return $this->fallbackResponse;
2358 - } else {
2359 - $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
2360 -
2361 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2362 -
2363 - $this->fallbackResponse = [
2364 - 'text' => $response_text,
2365 - 'html' => '',
2366 - 'images' => []
2367 - ];
2368 -
2369 - return $this->fallbackResponse;
2370 - }
2371 -}
2372 -
2373 -private function mxchat_save_generated_image($base64_data, $mime_type = 'image/png', $prefix = 'mxchat-generated') {
2374 - $extension = ($mime_type === 'image/jpeg') ? 'jpg' : 'png';
2375 - $filename = sanitize_file_name($prefix . '-' . wp_generate_uuid4() . '.' . $extension);
2376 - $decoded = base64_decode($base64_data);
2377 -
2378 - if ($decoded === false) {
2379 - return new \WP_Error('decode_failed', esc_html__('Failed to decode image data.', 'mxchat'));
2380 - }
2381 -
2382 - $upload = wp_upload_bits($filename, null, $decoded);
2383 -
2384 - if (!empty($upload['error'])) {
2385 - return new \WP_Error('upload_failed', $upload['error']);
2386 - }
2387 -
2388 - $attach_id = wp_insert_attachment([
2389 - 'post_mime_type' => $mime_type,
2390 - 'post_title' => $prefix,
2391 - 'post_content' => '',
2392 - 'post_status' => 'inherit',
2393 - ], $upload['file']);
2394 -
2395 - if (is_wp_error($attach_id)) {
2396 - return $attach_id;
2397 - }
2398 -
2399 - require_once ABSPATH . 'wp-admin/includes/image.php';
2400 - $metadata = wp_generate_attachment_metadata($attach_id, $upload['file']);
2401 - wp_update_attachment_metadata($attach_id, $metadata);
2402 -
2403 - return esc_url_raw(wp_get_attachment_url($attach_id));
2404 -}
2405 -
2406 -private function mxchat_generate_openai_image($prompt, $api_key, $model = 'gpt-image-1', $timeout = 60) {
2143 +private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
2407 2144 $api_url = 'https://api.openai.com/v1/images/generations';
2408 2145 $body = json_encode([
2409 - 'prompt' => sanitize_text_field($prompt),
2410 - 'n' => 1,
2411 - 'size' => '1024x1024',
2412 - 'quality' => 'medium',
2413 - 'output_format' => 'png',
2414 - 'model' => sanitize_text_field($model),
2146 + 'prompt' => sanitize_text_field($prompt),
2147 + 'n' => 1,
2148 + 'size' => '1024x1024',
2149 + 'model' => sanitize_text_field($model),
2415 2150 ]);
2416 2151
2417 2152 $args = [
2418 - 'body' => $body,
2153 + 'body' => $body,
2419 2154 'headers' => [
2420 - 'Content-Type' => 'application/json',
2155 + 'Content-Type' => 'application/json',
2421 2156 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
2422 2157 ],
2423 - 'method' => 'POST',
2158 + 'method' => 'POST',
2424 2159 'timeout' => absint($timeout),
2425 2160 ];
2426 2161
2427 2162 $response = wp_remote_post($api_url, $args);
@@ -2426,67 +2161,22 @@
2426 2161
2427 2162 $response = wp_remote_post($api_url, $args);
2428 2163
2429 2164 if (is_wp_error($response)) {
2165 + //error_log("DALL-E request failed: " . $response->get_error_message());
2430 2166 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2431 2167 }
2432 2168
2433 2169 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2434 2170
2435 - $b64 = $response_body['data'][0]['b64_json'] ?? $response_body['data'][0]['b64'] ?? null;
2436 - if ($b64) {
2437 - $saved_url = $this->mxchat_save_generated_image($b64, 'image/png', 'mxchat-openai');
2438 - if (is_wp_error($saved_url)) {
2439 - return ['error' => $saved_url->get_error_message()];
2440 - }
2441 - return ['imageUrl' => $saved_url];
2171 + if (isset($response_body['data'][0]['url'])) {
2172 + return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
2442 2173 } else {
2174 + //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
2443 2175 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2444 2176 }
2445 2177 }
2446 2178
2447 -private function mxchat_generate_imagen_image($prompt, $api_key, $timeout = 60) {
2448 - $api_url = 'https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict';
2449 -
2450 - $body = json_encode([
2451 - 'instances' => [['prompt' => sanitize_text_field($prompt)]],
2452 - 'parameters' => [
2453 - 'sampleCount' => 1,
2454 - 'aspectRatio' => '1:1',
2455 - ],
2456 - ]);
2457 -
2458 - $args = [
2459 - 'body' => $body,
2460 - 'headers' => [
2461 - 'Content-Type' => 'application/json',
2462 - 'x-goog-api-key' => sanitize_text_field($api_key),
2463 - ],
2464 - 'method' => 'POST',
2465 - 'timeout' => absint($timeout),
2466 - ];
2467 -
2468 - $response = wp_remote_post($api_url, $args);
2469 -
2470 - if (is_wp_error($response)) {
2471 - return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2472 - }
2473 -
2474 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
2475 -
2476 - $b64 = $response_body['predictions'][0]['bytesBase64Encoded'] ?? $response_body['predictions'][0]['imageBytes'] ?? null;
2477 - if ($b64) {
2478 - $mime = $response_body['predictions'][0]['mimeType'] ?? 'image/png';
2479 - $saved_url = $this->mxchat_save_generated_image($b64, $mime, 'mxchat-gemini');
2480 - if (is_wp_error($saved_url)) {
2481 - return ['error' => $saved_url->get_error_message()];
2482 - }
2483 - return ['imageUrl' => $saved_url];
2484 - } else {
2485 - return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2486 - }
2487 -}
2488 -
2489 2179 /**
2490 2180 * Handle web search requests.
2491 2181 *
2492 2182 * Sends the refined search query to the Brave Search API and uses the
@@ -2753,9 +2443,9 @@
2753 2443 $system_prompt = esc_html__("Interpret the user's request to provide only the essential keywords or phrases for image searching. Remove conversational language, politeness, or extra context. Return a concise search query that doesn't lose any of the original meaning.", 'mxchat');
2754 2444
2755 2445 // Get options and determine the selected model
2756 2446 $options = $this->options ?? get_option('mxchat_options');
2757 - $selected_model = isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest';
2447 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
2758 2448
2759 2449 // Extract model prefix to determine the provider
2760 2450 $model_parts = explode('-', $selected_model);
2761 2451 $provider = strtolower($model_parts[0]);
@@ -2803,9 +2493,9 @@
2803 2493
2804 2494 /**
2805 2495 * Interpret query using OpenAI models
2806 2496 */
2807 -private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-5.1-chat-latest') {
2497 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
2808 2498 $url = 'https://api.openai.com/v1/chat/completions';
2809 2499 $args = [
2810 2500 'headers' => [
2811 2501 'Authorization' => 'Bearer ' . $api_key,
@@ -2876,13 +2566,13 @@
2876 2566 /**
2877 2567 * Interpret query using Gemini models
2878 2568 */
2879 2569 private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
2880 - // Use v1beta for preview models, v1 for stable models
2881 - $api_version = (strpos($model, 'preview') !== false || strpos($model, 'exp') !== false) ? 'v1beta' : 'v1';
2882 -
2883 - $url = "https://generativelanguage.googleapis.com/{$api_version}/models/{$model}:generateContent?key=" . urlencode($api_key);
2570 + // Strip "gemini-" prefix for the API
2571 + $model_version = str_replace('gemini-', '', $model);
2884 2572
2573 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
2574 +
2885 2575 $args = [
2886 2576 'headers' => [
2887 2577 'Content-Type' => 'application/json',
2888 2578 ],
@@ -3156,14 +2846,9 @@
3156 2846 //error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
3157 2847 return false;
3158 2848 }
3159 2849
3160 - global $wp_filesystem;
3161 - if (empty($wp_filesystem)) {
3162 - require_once ABSPATH . 'wp-admin/includes/file.php';
3163 - WP_Filesystem();
3164 - }
3165 - $wp_filesystem->put_contents($temp_file, wp_remote_retrieve_body($response), FS_CHMOD_FILE);
2850 + file_put_contents($temp_file, wp_remote_retrieve_body($response));
3166 2851 //error_log("✅ PDF downloaded successfully");
3167 2852 } else {
3168 2853 $temp_file = $pdf_source;
3169 2854 //error_log("Using local PDF file: " . $temp_file);
@@ -3170,9 +2855,8 @@
3170 2855 }
3171 2856
3172 2857 // Parse PDF
3173 2858 //error_log("Parsing PDF with basic parser...");
3174 - mxchat_load_pdf_parser();
3175 2859 $parser = new \Smalot\PdfParser\Parser();
3176 2860 $pdf = $parser->parseFile($temp_file);
3177 2861 $pages = $pdf->getPages();
3178 2862
@@ -3323,14 +3007,15 @@
3323 3007 $file = $_FILES['pdf_file'];
3324 3008 $session_id = sanitize_text_field($_POST['session_id']);
3325 3009 $original_filename = sanitize_text_field($file['name']);
3326 3010
3327 - // Update session owner if it changed (e.g. IP changed due to network switch)
3011 + // SECURITY FIX: Verify session ownership before allowing upload
3328 3012 $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
3329 3013 $session_owner = get_option("mxchat_session_owner_{$session_id}");
3330 -
3331 - if (!$session_owner || $session_owner !== $current_user_identifier) {
3332 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
3014 +
3015 + if ($session_owner && $session_owner !== $current_user_identifier) {
3016 + wp_send_json_error(esc_html__('Unauthorized access.', 'mxchat'));
3017 + return;
3333 3018 }
3334 3019
3335 3020 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
3336 3021 if ($file_type['type'] !== 'application/pdf') {
@@ -3434,15 +3119,15 @@
3434 3119 }
3435 3120
3436 3121 $history = get_option("mxchat_history_{$session_id}", []);
3437 3122
3438 - //error_log("MxChat WhatsApp DEBUG: Fetch new messages for session {$session_id}");
3439 - //error_log("MxChat WhatsApp DEBUG: last_seen_id = " . var_export($last_seen_id, true));
3440 - //error_log("MxChat WhatsApp DEBUG: History count = " . count($history));
3441 - //error_log("MxChat WhatsApp DEBUG: Full history = " . print_r($history, true));
3123 + error_log("MxChat WhatsApp DEBUG: Fetch new messages for session {$session_id}");
3124 + error_log("MxChat WhatsApp DEBUG: last_seen_id = " . var_export($last_seen_id, true));
3125 + error_log("MxChat WhatsApp DEBUG: History count = " . count($history));
3126 + error_log("MxChat WhatsApp DEBUG: Full history = " . print_r($history, true));
3442 3127
3443 3128 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
3444 - //error_log("MxChat WhatsApp DEBUG: Checking message - ID: " . ($message['id'] ?? 'NO_ID') . ", Role: " . ($message['role'] ?? 'NO_ROLE'));
3129 + error_log("MxChat WhatsApp DEBUG: Checking message - ID: " . ($message['id'] ?? 'NO_ID') . ", Role: " . ($message['role'] ?? 'NO_ROLE'));
3445 3130
3446 3131 // If persistence is enabled, show all new messages
3447 3132 if ($persistence_enabled) {
3448 3133 $has_id = !empty($message['id']);
@@ -3454,9 +3139,9 @@
3454 3139 } else {
3455 3140 $is_newer = strcmp($message['id'] ?? '', $last_seen_id) > 0;
3456 3141 }
3457 3142
3458 - //error_log("MxChat WhatsApp DEBUG: has_id={$has_id}, is_newer={$is_newer}, is_agent={$is_agent}");
3143 + error_log("MxChat WhatsApp DEBUG: has_id={$has_id}, is_newer={$is_newer}, is_agent={$is_agent}");
3459 3144
3460 3145 return $has_id && $is_newer && $is_agent;
3461 3146 }
3462 3147
@@ -3465,16 +3150,12 @@
3465 3150 $message['role'] === 'agent' &&
3466 3151 $message['timestamp'] > $initial_timestamp;
3467 3152 });
3468 3153
3469 - //error_log("MxChat WhatsApp DEBUG: Filtered messages count = " . count($new_messages));
3154 + error_log("MxChat WhatsApp DEBUG: Filtered messages count = " . count($new_messages));
3470 3155
3471 - // Include current chat mode so frontend can detect agent→AI transitions
3472 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
3473 -
3474 3156 wp_send_json_success([
3475 - 'new_messages' => array_values($new_messages),
3476 - 'chat_mode' => $chat_mode
3157 + 'new_messages' => array_values($new_messages)
3477 3158 ]);
3478 3159 wp_die();
3479 3160 }
3480 3161 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
@@ -3759,393 +3440,9 @@
3759 3440
3760 3441 //error_log("[DEBUG] Generated channel name: {$channel_name}");
3761 3442 return $channel_name;
3762 3443 }
3763 -
3764 -/**
3765 - * Telegram Live Agent Handover
3766 - * Creates a forum topic in the Telegram group and notifies agents
3767 - */
3768 -public function mxchat_telegram_live_agent_handover($message, $user_id, $session_id) {
3769 - // Check if Telegram agents are available
3770 - $telegram_available = $this->options['telegram_status'] ?? 'off';
3771 - if ($telegram_available !== 'on') {
3772 - $away_message = $this->options['telegram_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
3773 - $this->fallbackResponse = [
3774 - 'text' => $away_message,
3775 - 'html' => '',
3776 - 'images' => [],
3777 - 'chat_mode' => 'ai'
3778 - ];
3779 - wp_send_json([
3780 - 'text' => $away_message,
3781 - 'html' => '',
3782 - 'chat_mode' => 'ai',
3783 - 'session_id' => $session_id
3784 - ]);
3785 - wp_die();
3786 - }
3787 -
3788 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
3789 - $telegram_group_id = $this->options['telegram_group_id'] ?? '';
3790 -
3791 - if (empty($telegram_bot_token) || empty($telegram_group_id)) {
3792 - return false;
3793 - }
3794 -
3795 - // Check if topic already exists for this session
3796 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
3797 -
3798 - if (empty($topic_id)) {
3799 - // Generate topic name
3800 - $topic_name = $this->generate_telegram_topic_name($session_id);
3801 -
3802 - // Random icon color (Telegram forum topic colors)
3803 - $icon_colors = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, 0xFB6F5F];
3804 - $icon_color = $icon_colors[array_rand($icon_colors)];
3805 -
3806 - // Create forum topic
3807 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/createForumTopic", [
3808 - 'headers' => ['Content-Type' => 'application/json'],
3809 - 'body' => json_encode([
3810 - 'chat_id' => $telegram_group_id,
3811 - 'name' => $topic_name,
3812 - 'icon_color' => $icon_color
3813 - ])
3814 - ]);
3815 -
3816 - if (!is_wp_error($response)) {
3817 - $response_body = wp_remote_retrieve_body($response);
3818 - $response_data = json_decode($response_body, true);
3819 -
3820 - if (isset($response_data['ok']) && $response_data['ok']) {
3821 - $topic_id = $response_data['result']['message_thread_id'];
3822 - update_option("mxchat_telegram_topic_{$session_id}", $topic_id);
3823 - update_option("mxchat_telegram_group_{$session_id}", $telegram_group_id);
3824 - }
3825 - }
3826 -
3827 - if (empty($topic_id)) {
3828 - return false; // Failed to create topic
3829 - }
3830 - }
3831 -
3832 - // Get recent chat history
3833 - $history = get_option("mxchat_history_{$session_id}", []);
3834 - $recent_history = array_slice($history, -5);
3835 -
3836 - // Format conversation context for Telegram (HTML format)
3837 - $conversation_context = "";
3838 - if (!empty($recent_history)) {
3839 - $conversation_context = "<b>Recent Conversation:</b>\n";
3840 - foreach ($recent_history as $hist_message) {
3841 - $role_display = $hist_message['role'] === 'user' ? '👤 User' : '🤖 AI';
3842 - $escaped_content = htmlspecialchars($hist_message['content'], ENT_QUOTES, 'UTF-8');
3843 - $conversation_context .= "{$role_display}: {$escaped_content}\n";
3844 - }
3845 - $conversation_context .= "\n";
3846 - }
3847 -
3848 - // Get user info
3849 - $user_email = get_option("mxchat_email_{$session_id}", 'Not provided');
3850 - $user_name = get_option("mxchat_name_{$session_id}", 'Anonymous');
3851 -
3852 - // Update session mode
3853 - update_option("mxchat_mode_{$session_id}", 'agent');
3854 -
3855 - // Send initial message to topic
3856 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3857 - $topic_message = "🔔 <b>New Live Agent Request</b>\n\n";
3858 - $topic_message .= "<b>Session ID:</b> <code>{$session_id}</code>\n";
3859 - $topic_message .= "<b>User:</b> {$user_name}\n";
3860 - $topic_message .= "<b>Email:</b> {$user_email}\n\n";
3861 -
3862 - if (!empty($conversation_context)) {
3863 - $topic_message .= $conversation_context;
3864 - }
3865 -
3866 - $topic_message .= "<b>Current Message:</b>\n{$escaped_message}\n\n";
3867 - $topic_message .= "<i>Reply in this topic - messages will be sent to the user</i>\n";
3868 - $topic_message .= "<i>Type #close, #end, #disconnect, or #done to end the session</i>";
3869 -
3870 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
3871 - 'headers' => ['Content-Type' => 'application/json'],
3872 - 'body' => json_encode([
3873 - 'chat_id' => $telegram_group_id,
3874 - 'message_thread_id' => $topic_id,
3875 - 'text' => $topic_message,
3876 - 'parse_mode' => 'HTML'
3877 - ])
3878 - ]);
3879 -
3880 - $success_message = $this->options['telegram_notification_message'] ?? "I've notified a support agent. Please allow a moment for them to respond.";
3881 - $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
3882 -
3883 - $this->fallbackResponse = [
3884 - 'text' => $success_message,
3885 - 'html' => '',
3886 - 'images' => [],
3887 - 'chat_mode' => 'agent'
3888 - ];
3889 -
3890 - wp_send_json([
3891 - 'success' => true,
3892 - 'text' => $success_message,
3893 - 'html' => '',
3894 - 'chat_mode' => 'agent',
3895 - 'session_id' => $session_id,
3896 - 'fallbackResponse' => $this->fallbackResponse
3897 - ]);
3898 - wp_die();
3899 -}
3900 -
3901 -/**
3902 - * Generate topic name for Telegram forum
3903 - */
3904 -private function generate_telegram_topic_name($session_id) {
3905 - $name = null;
3906 - $email = null;
3907 -
3908 - // Check logged in user
3909 - if (is_user_logged_in()) {
3910 - $current_user = wp_get_current_user();
3911 - if (!empty($current_user->display_name)) {
3912 - $name = $current_user->display_name;
3913 - }
3914 - if (!empty($current_user->user_email)) {
3915 - $email = $current_user->user_email;
3916 - }
3917 - }
3918 -
3919 - // Check session data
3920 - if (empty($name)) {
3921 - $name = get_option("mxchat_name_{$session_id}");
3922 - }
3923 - if (empty($email)) {
3924 - $email = get_option("mxchat_email_{$session_id}");
3925 - }
3926 -
3927 - // Generate topic name
3928 - $session_suffix = substr($session_id, -6);
3929 -
3930 - if (!empty($name)) {
3931 - // Clean name for topic (max 128 chars in Telegram)
3932 - $clean_name = preg_replace('/[^\p{L}\p{N}\s\-]/u', '', $name);
3933 - $clean_name = trim($clean_name);
3934 - if (strlen($clean_name) > 50) {
3935 - $clean_name = substr($clean_name, 0, 50);
3936 - }
3937 - return "Chat - {$clean_name} ({$session_suffix})";
3938 - } elseif (!empty($email)) {
3939 - // Use email prefix
3940 - $email_prefix = explode('@', $email)[0];
3941 - if (strlen($email_prefix) > 30) {
3942 - $email_prefix = substr($email_prefix, 0, 30);
3943 - }
3944 - return "Chat - {$email_prefix} ({$session_suffix})";
3945 - }
3946 -
3947 - return "Chat - {$session_suffix}";
3948 -}
3949 -
3950 -/**
3951 - * Send user message to Telegram agent
3952 - */
3953 -public function mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id) {
3954 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
3955 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
3956 - $group_id = get_option("mxchat_telegram_group_{$session_id}", '');
3957 -
3958 - if (empty($telegram_bot_token) || empty($topic_id) || empty($group_id)) {
3959 - return false;
3960 - }
3961 -
3962 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3963 - $user_message = "👤 <b>User:</b> {$escaped_message}";
3964 -
3965 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
3966 - 'headers' => ['Content-Type' => 'application/json'],
3967 - 'body' => json_encode([
3968 - 'chat_id' => $group_id,
3969 - 'message_thread_id' => $topic_id,
3970 - 'text' => $user_message,
3971 - 'parse_mode' => 'HTML'
3972 - ])
3973 - ]);
3974 -
3975 - return !is_wp_error($response);
3976 -}
3977 -
3978 -/**
3979 - * Handle incoming Telegram webhook
3980 - */
3981 -public function handle_telegram_webhook(WP_REST_Request $request) {
3982 - $body = $request->get_body();
3983 - $data = json_decode($body, true);
3984 -
3985 - //error_log('[MxChat Telegram DEBUG] Webhook received: ' . $body);
3986 -
3987 - // Handle message events from forum topics
3988 - if (isset($data['message'])) {
3989 - $message_data = $data['message'];
3990 -
3991 - // Skip if not from a forum topic
3992 - if (!isset($message_data['message_thread_id'])) {
3993 - //error_log('[MxChat Telegram DEBUG] Skipped: No message_thread_id (not a forum topic message)');
3994 - return new WP_REST_Response(['ok' => true]);
3995 - }
3996 -
3997 - // Skip bot messages
3998 - if (isset($message_data['from']['is_bot']) && $message_data['from']['is_bot']) {
3999 - //error_log('[MxChat Telegram DEBUG] Skipped: Message from bot');
4000 - return new WP_REST_Response(['ok' => true]);
4001 - }
4002 -
4003 - $chat_id = $message_data['chat']['id'] ?? '';
4004 - $topic_id = $message_data['message_thread_id'];
4005 - $message_text = $message_data['text'] ?? '';
4006 - $message_id = $message_data['message_id'] ?? '';
4007 - $from = $message_data['from'] ?? [];
4008 - $agent_name = trim(($from['first_name'] ?? '') . ' ' . ($from['last_name'] ?? ''));
4009 - if (empty($agent_name)) {
4010 - $agent_name = $from['username'] ?? 'Agent';
4011 - }
4012 -
4013 - //error_log("[MxChat Telegram DEBUG] Parsed: chat_id={$chat_id}, topic_id={$topic_id}, agent={$agent_name}, text={$message_text}");
4014 -
4015 - // Skip empty messages
4016 - if (empty($message_text)) {
4017 - //error_log('[MxChat Telegram DEBUG] Skipped: Empty message text');
4018 - return new WP_REST_Response(['ok' => true]);
4019 - }
4020 -
4021 - // Find session ID by topic ID - cast to string for comparison
4022 - global $wpdb;
4023 - $topic_id_str = strval($topic_id);
4024 - $session_option = $wpdb->get_var(
4025 - $wpdb->prepare(
4026 - "SELECT option_name FROM {$wpdb->options}
4027 - WHERE option_name LIKE %s
4028 - AND option_value = %s",
4029 - 'mxchat_telegram_topic_%',
4030 - $topic_id_str
4031 - )
4032 - );
4033 -
4034 - //error_log("[MxChat Telegram DEBUG] Looking for topic_id={$topic_id_str} in options, found: " . ($session_option ?: 'NULL'));
4035 -
4036 - if ($session_option) {
4037 - $session_id = str_replace('mxchat_telegram_topic_', '', $session_option);
4038 - //error_log("[MxChat Telegram DEBUG] Session ID: {$session_id}");
4039 -
4040 - // Verify the group ID matches
4041 - $stored_group_id = get_option("mxchat_telegram_group_{$session_id}", '');
4042 - //error_log("[MxChat Telegram DEBUG] Stored group_id={$stored_group_id}, received chat_id={$chat_id}");
4043 -
4044 - if (strval($stored_group_id) != strval($chat_id)) {
4045 - //error_log('[MxChat Telegram DEBUG] Skipped: Group ID mismatch');
4046 - return new WP_REST_Response(['ok' => true]);
4047 - }
4048 -
4049 - // Check for closure commands
4050 - $lower_text = strtolower(trim($message_text));
4051 - if (in_array($lower_text, ['#close', '#end', '#disconnect', '#done'])) {
4052 - //error_log("[MxChat Telegram DEBUG] Closure command received: {$lower_text}");
4053 - // End the live agent session
4054 - update_option("mxchat_mode_{$session_id}", 'ai');
4055 -
4056 - // Save disconnect message
4057 - $disconnect_message = "Live agent session ended. You're now chatting with the AI assistant.";
4058 - $this->mxchat_save_chat_message($session_id, 'bot', $disconnect_message);
4059 -
4060 - // Notify in Telegram
4061 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4062 - if (!empty($telegram_bot_token)) {
4063 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4064 - 'headers' => ['Content-Type' => 'application/json'],
4065 - 'body' => json_encode([
4066 - 'chat_id' => $chat_id,
4067 - 'message_thread_id' => $topic_id,
4068 - 'text' => "✅ Session closed. User returned to AI chatbot.",
4069 - 'parse_mode' => 'HTML'
4070 - ])
4071 - ]);
4072 -
4073 - // Optionally close the topic
4074 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/closeForumTopic", [
4075 - 'headers' => ['Content-Type' => 'application/json'],
4076 - 'body' => json_encode([
4077 - 'chat_id' => $chat_id,
4078 - 'message_thread_id' => $topic_id
4079 - ])
4080 - ]);
4081 - }
4082 -
4083 - return new WP_REST_Response(['ok' => true]);
4084 - }
4085 -
4086 - // Deduplicate messages
4087 - $message_key = md5($session_id . $message_id . $message_text);
4088 - $processed_messages = get_transient('mxchat_telegram_messages_' . $session_id) ?: [];
4089 -
4090 - if (in_array($message_key, $processed_messages)) {
4091 - //error_log('[MxChat Telegram DEBUG] Skipped: Duplicate message');
4092 - return new WP_REST_Response(['ok' => true]);
4093 - }
4094 -
4095 - $processed_messages[] = $message_key;
4096 - if (count($processed_messages) > 50) {
4097 - $processed_messages = array_slice($processed_messages, -50);
4098 - }
4099 - set_transient('mxchat_telegram_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4100 -
4101 - // Save the agent message - format with agent name prefix for proper parsing
4102 - $formatted_message = "Agent: {$agent_name} - {$message_text}";
4103 - //error_log("[MxChat Telegram DEBUG] Saving agent message: {$formatted_message}");
4104 -
4105 - $this->mxchat_save_chat_message($session_id, 'agent', $formatted_message);
4106 -
4107 - // Verify the message was saved to history
4108 - $history = get_option("mxchat_history_{$session_id}", []);
4109 - $last_message = end($history);
4110 - //error_log("[MxChat Telegram DEBUG] History after save - count: " . count($history) . ", last message role: " . ($last_message['role'] ?? 'none'));
4111 -
4112 - // Send confirmation back to Telegram
4113 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4114 - if (!empty($telegram_bot_token)) {
4115 - $confirm_key = 'mxchat_telegram_confirm_' . $message_key;
4116 - if (!get_transient($confirm_key)) {
4117 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4118 - 'headers' => ['Content-Type' => 'application/json'],
4119 - 'body' => json_encode([
4120 - 'chat_id' => $chat_id,
4121 - 'message_thread_id' => $topic_id,
4122 - 'text' => "✅ <i>Message sent to user</i>",
4123 - 'parse_mode' => 'HTML',
4124 - 'reply_to_message_id' => $message_id
4125 - ])
4126 - ]);
4127 - set_transient($confirm_key, true, 300);
4128 - }
4129 - }
4130 - } else {
4131 - //error_log("[MxChat Telegram DEBUG] No session found for topic_id={$topic_id}");
4132 - }
4133 - } else {
4134 - //error_log('[MxChat Telegram DEBUG] No message in webhook data');
4135 - }
4136 -
4137 - return new WP_REST_Response(['ok' => true]);
4138 -}
4139 -
4140 3444 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
4141 - // Check if this is a Telegram agent session
4142 - $telegram_topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4143 - if (!empty($telegram_topic_id)) {
4144 - return $this->mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id);
4145 - }
4146 -
4147 - // Otherwise, try Slack
4148 3445 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4149 3446 $channel_id = get_option("mxchat_channel_{$session_id}", '');
4150 3447
4151 3448 if (empty($slack_bot_token) || empty($channel_id)) {
@@ -4371,33 +3668,33 @@
4371 3668
4372 3669 $channel_id = $event['channel'];
4373 3670 $message_text = $event['text'] ?? '';
4374 3671 $message_ts = $event['ts'] ?? '';
4375 -
3672 +
4376 3673 // Find session ID by looking for matching channel
4377 3674 global $wpdb;
4378 3675 $session_option = $wpdb->get_var(
4379 3676 $wpdb->prepare(
4380 - "SELECT option_name FROM {$wpdb->options}
4381 - WHERE option_name LIKE 'mxchat_channel_%'
3677 + "SELECT option_name FROM {$wpdb->options}
3678 + WHERE option_name LIKE 'mxchat_channel_%'
4382 3679 AND option_value = %s",
4383 3680 $channel_id
4384 3681 )
4385 3682 );
4386 -
3683 +
4387 3684 if ($session_option) {
4388 3685 $session_id = str_replace('mxchat_channel_', '', $session_option);
4389 -
3686 +
4390 3687 // Create a unique key for this specific message
4391 3688 $message_key = md5($session_id . $message_ts . $message_text);
4392 3689 $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
4393 -
3690 +
4394 3691 // Check if we've already processed this exact message
4395 3692 if (in_array($message_key, $processed_messages)) {
4396 3693 //error_log("Duplicate message detected for session $session_id");
4397 3694 return new WP_REST_Response(['ok' => true]);
4398 3695 }
4399 -
3696 +
4400 3697 // Add to processed messages
4401 3698 $processed_messages[] = $message_key;
4402 3699 // Keep only last 50 messages per session
4403 3700 if (count($processed_messages) > 50) {
@@ -4403,46 +3700,14 @@
4403 3700 if (count($processed_messages) > 50) {
4404 3701 $processed_messages = array_slice($processed_messages, -50);
4405 3702 }
4406 3703 set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4407 -
4408 - $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4409 -
4410 - // Handle agent ending the chat — transfer back to AI
4411 - // Format: "!endchat" or "!endchat <custom message to user>"
4412 - if (preg_match('/^!endchat\b/i', trim($message_text))) {
4413 - update_option("mxchat_mode_{$session_id}", 'ai');
4414 -
4415 - // Extract custom message after !endchat, or use empty string
4416 - $custom_message = trim(preg_replace('/^!endchat\s*/i', '', trim($message_text)));
4417 -
4418 - // Send the agent's custom farewell message if provided
4419 - if (!empty($custom_message)) {
4420 - $this->mxchat_save_chat_message($session_id, 'agent', $custom_message);
4421 - }
4422 -
4423 - // Confirm in Slack channel
4424 - if (!empty($slack_bot_token)) {
4425 - wp_remote_post('https://slack.com/api/chat.postMessage', [
4426 - 'headers' => [
4427 - 'Content-Type' => 'application/json',
4428 - 'Authorization' => 'Bearer ' . $slack_bot_token
4429 - ],
4430 - 'body' => json_encode([
4431 - 'channel' => $channel_id,
4432 - 'text' => "✅ *Chat ended.* User has been transferred back to AI mode.",
4433 - 'mrkdwn' => true
4434 - ])
4435 - ]);
4436 - }
4437 -
4438 - return new WP_REST_Response(['ok' => true]);
4439 - }
4440 -
3704 +
4441 3705 // Save the agent message
4442 3706 $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
4443 -
3707 +
4444 3708 // Send confirmation back to Slack (only once)
3709 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4445 3710 if (!empty($slack_bot_token)) {
4446 3711 // Use a transient to prevent duplicate confirmations
4447 3712 $confirm_key = 'mxchat_confirm_' . $message_key;
4448 3713 if (!get_transient($confirm_key)) {
@@ -4452,9 +3717,9 @@
4452 3717 'Authorization' => 'Bearer ' . $slack_bot_token
4453 3718 ],
4454 3719 'body' => json_encode([
4455 3720 'channel' => $channel_id,
4456 - 'text' => "✅ _Message sent to user_",
3721 + 'text' => "✅ _Message sent to user_",
4457 3722 'thread_ts' => $event['ts'] // Reply in thread
4458 3723 ])
4459 3724 ]);
4460 3725 // Set transient to prevent duplicate confirmations
@@ -4693,44 +3958,26 @@
4693 3958 }
4694 3959 }
4695 3960
4696 3961
4697 -private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default', $user_query = '') {
4698 - //error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
4699 -
4700 - // Check for OpenAI Vector Store first (takes priority when enabled)
4701 - $bot_vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
4702 -
4703 - if ($bot_vectorstore_config['use_vectorstore']) {
4704 - // Get current model to verify it's an OpenAI model
4705 - $bot_options = $this->get_bot_options($bot_id);
4706 - $mxchat_options = get_option('mxchat_options', array());
4707 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
4708 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
4709 -
4710 - if ($this->is_openai_chat_model($selected_model)) {
4711 - //error_log("MXCHAT DEBUG: Using OpenAI Vector Store for knowledge retrieval");
4712 - return $this->find_relevant_content_openai_vectorstore($user_query, $bot_id, $bot_vectorstore_config);
4713 - } else {
4714 - //error_log("MXCHAT DEBUG: Vector Store enabled but model is not OpenAI (" . $selected_model . "), skipping Vector Store");
4715 - }
4716 - }
4717 -
3962 +private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default') {
3963 + error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
3964 +
4718 3965 // Get bot-specific Pinecone configuration
4719 3966 $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
4720 -
3967 +
4721 3968 // Debug: Log the Pinecone configuration
4722 - //error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
4723 - //error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
4724 - //error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
4725 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
4726 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
4727 -
3969 + error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
3970 + error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
3971 + error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
3972 + error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
3973 + error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
3974 +
4728 3975 // Determine whether to use Pinecone based on bot configuration
4729 3976 $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
3977 +
3978 + error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
4730 3979
4731 - //error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
4732 -
4733 3980 if ($use_pinecone) {
4734 3981 return $this->find_relevant_content_pinecone($user_embedding, $bot_id, $bot_pinecone_config);
4735 3982 } else {
4736 3983 return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
@@ -4739,8 +3986,11 @@
4739 3986
4740 3987 private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
4741 3988 global $wpdb;
4742 3989 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3990 + $cache_key = 'mxchat_system_prompt_embeddings_' . $bot_id;
3991 + $batch_size = 500;
3992 +
4743 3993 // Initialize similarity analysis storage
4744 3994 $this->last_similarity_analysis = [
4745 3995 'knowledge_base_type' => 'WordPress Database',
4746 3996 'bot_id' => $bot_id,
@@ -4755,467 +4005,263 @@
4755 4005 // Get bot-specific options for similarity threshold
4756 4006 $bot_options = $this->get_bot_options($bot_id);
4757 4007 $current_options = !empty($bot_options) ? $bot_options : $this->options;
4758 4008
4759 - // Get knowledge manager instance for role checking
4760 - $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
4009 + // Retrieve embeddings from cache or database
4010 + $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
4011 + if ($embeddings === false) {
4012 + // Cache miss - load embeddings from database WITH CONTENT and ROLE RESTRICTION for testing
4013 + $embeddings = [];
4014 + $offset = 0;
4761 4015
4762 - // Get base similarity threshold from bot options or default options
4763 - $similarity_threshold = isset($current_options['similarity_threshold'])
4764 - ? ((int) $current_options['similarity_threshold']) / 100
4765 - : 0.35;
4766 - $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
4016 + do {
4017 + // Add bot_id filter if not default and if bot_metadata column exists
4018 + $bot_filter = '';
4019 + if ($bot_id !== 'default') {
4020 + // Check if bot_metadata column exists
4021 + $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
4022 + if ($column_exists) {
4023 + $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
4024 + }
4025 + }
4767 4026
4768 - // Precompute bot_filter once, outside the streaming loop
4769 - $bot_filter = '';
4770 - if ($bot_id !== 'default') {
4771 - $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
4772 - if ($column_exists) {
4773 - $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
4774 - }
4775 - }
4027 + $query = $wpdb->prepare(
4028 + "SELECT id, embedding_vector, article_content, source_url, role_restriction
4029 + FROM {$system_prompt_table}
4030 + WHERE 1=1 {$bot_filter}
4031 + LIMIT %d OFFSET %d",
4032 + $batch_size,
4033 + $offset
4034 + );
4776 4035
4777 - // ===== STREAMING TOP-K PASS =====
4778 - // Stream rows in small batches, compute cosine similarity per row, and keep only:
4779 - // - top 10 by raw similarity (for the testing/debug display panel)
4780 - // - candidates above threshold with access (capped) for context assembly
4781 - // This bounds peak memory regardless of knowledge base size and avoids loading
4782 - // article_content for every row. article_content is fetched in Phase 2 for winners only.
4783 - $batch_size = 250;
4784 - $max_candidates = 200; // safety cap, well above rag_sources_limit * max_chunks_per_source
4785 - $top_display = [];
4786 - $candidates = [];
4787 - $total_checked = 0;
4788 - $offset = 0;
4036 + $batch = $wpdb->get_results($query);
4037 + if (empty($batch)) {
4038 + break;
4039 + }
4789 4040
4790 - do {
4791 - $batch = $wpdb->get_results($wpdb->prepare(
4792 - "SELECT id, embedding_vector, source_url, role_restriction
4793 - FROM {$system_prompt_table}
4794 - WHERE 1=1 {$bot_filter}
4795 - LIMIT %d OFFSET %d",
4796 - $batch_size,
4797 - $offset
4798 - ));
4041 + $embeddings = array_merge($embeddings, $batch);
4042 + $offset += $batch_size;
4043 + unset($batch);
4044 + } while (true);
4799 4045
4800 - if (empty($batch)) {
4801 - break;
4046 + if (empty($embeddings)) {
4047 + // Store empty array for valid URLs since no content found
4048 + $this->current_valid_urls = [];
4049 + return '';
4802 4050 }
4051 +
4052 + // Cache embeddings for future use (but note: this now includes content and role restrictions)
4053 + wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
4054 + }
4803 4055
4804 - foreach ($batch as $row) {
4805 - $database_embedding = $row->embedding_vector
4806 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
4807 - : null;
4056 + // Get knowledge manager instance for role checking
4057 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
4808 4058
4809 - if (!is_array($database_embedding) || !is_array($user_embedding)) {
4810 - unset($database_embedding);
4811 - continue;
4812 - }
4813 -
4059 + // Get base similarity threshold from bot options or default options
4060 + $similarity_threshold = isset($current_options['similarity_threshold'])
4061 + ? ((int) $current_options['similarity_threshold']) / 100
4062 + : 0.35;
4063 +
4064 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
4065 +
4066 + // Calculate similarities and build results array
4067 + $all_similarities = [];
4068 + $relevant_results = [];
4069 +
4070 + foreach ($embeddings as $embedding) {
4071 + $database_embedding = $embedding->embedding_vector
4072 + ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
4073 + : null;
4074 +
4075 + if (is_array($database_embedding) && is_array($user_embedding)) {
4814 4076 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
4815 - unset($database_embedding);
4816 -
4817 - $role_restriction = $row->role_restriction ?? 'public';
4077 +
4078 + // Check role access
4079 + $role_restriction = $embedding->role_restriction ?? 'public';
4818 4080 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
4819 - $source_url = $row->source_url ?? '';
4820 -
4821 - // Maintain top 10 display buffer (insert-if-beats-worst)
4822 - if (count($top_display) < 10) {
4823 - $top_display[] = [
4824 - 'id' => $row->id,
4825 - 'similarity' => $similarity,
4826 - 'source_url' => $source_url,
4827 - 'role_restriction' => $role_restriction,
4828 - 'has_access' => $has_access,
4829 - ];
4830 - usort($top_display, function ($a, $b) {
4831 - return $b['similarity'] <=> $a['similarity'];
4832 - });
4833 - } elseif ($similarity > $top_display[9]['similarity']) {
4834 - $top_display[9] = [
4835 - 'id' => $row->id,
4836 - 'similarity' => $similarity,
4837 - 'source_url' => $source_url,
4838 - 'role_restriction' => $role_restriction,
4839 - 'has_access' => $has_access,
4840 - ];
4841 - usort($top_display, function ($a, $b) {
4842 - return $b['similarity'] <=> $a['similarity'];
4843 - });
4081 +
4082 + // Store ALL similarities for testing (top 10)
4083 + $source_display = '';
4084 + if (!empty($embedding->source_url) && $embedding->source_url !== '#') {
4085 + $source_display = $embedding->source_url;
4086 + } else {
4087 + $content_preview = strip_tags($embedding->article_content ?? '');
4088 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
4089 + $source_display = substr(trim($content_preview), 0, 50) . '...';
4844 4090 }
4845 -
4846 - // Track candidates for context assembly (above threshold + has access)
4091 +
4092 + $all_similarities[] = [
4093 + 'document_id' => $embedding->id,
4094 + 'similarity' => $similarity,
4095 + 'similarity_percentage' => round($similarity * 100, 2),
4096 + 'above_threshold' => $similarity >= $similarity_threshold,
4097 + 'source_display' => $source_display,
4098 + 'content_preview' => substr(strip_tags($embedding->article_content ?? ''), 0, 100) . '...',
4099 + 'used_for_context' => false, // Initialize as false, we'll update this later
4100 + 'role_restriction' => $role_restriction,
4101 + 'has_access' => $has_access,
4102 + 'filtered_out' => !$has_access
4103 + ];
4104 +
4105 + // Only consider results above threshold AND with access for actual content retrieval
4847 4106 if ($similarity >= $similarity_threshold && $has_access) {
4848 - $candidates[] = [
4849 - 'id' => $row->id,
4850 - 'similarity' => $similarity,
4851 - 'source_url' => $source_url,
4107 + $relevant_results[] = [
4108 + 'id' => $embedding->id,
4109 + 'similarity' => $similarity
4852 4110 ];
4853 4111 }
4854 -
4855 - $total_checked++;
4856 4112 }
4857 -
4858 - unset($batch);
4859 -
4860 - // Trim candidates periodically to cap memory during long scans
4861 - if (count($candidates) > $max_candidates) {
4862 - usort($candidates, function ($a, $b) {
4863 - return $b['similarity'] <=> $a['similarity'];
4864 - });
4865 - $candidates = array_slice($candidates, 0, $max_candidates);
4866 - }
4867 -
4868 - $offset += $batch_size;
4869 - } while (true);
4870 -
4871 - if ($total_checked === 0) {
4872 - $this->current_valid_urls = [];
4873 - return '';
4113 +
4114 + unset($database_embedding);
4874 4115 }
4875 4116
4876 - // Final candidates sort (best first)
4877 - if (count($candidates) > 1) {
4878 - usort($candidates, function ($a, $b) {
4879 - return $b['similarity'] <=> $a['similarity'];
4880 - });
4881 - }
4882 -
4883 - // ===== PHASE 2: FETCH ARTICLE CONTENT ONLY FOR WINNERS =====
4884 - // Gather unique IDs we actually need (top_display + candidates) and pull
4885 - // article_content in bounded IN() batches. This avoids loading content for
4886 - // every row during the similarity scan.
4887 - $needed_ids = [];
4888 - foreach ($top_display as $item) {
4889 - $needed_ids[$item['id']] = true;
4890 - }
4891 - foreach ($candidates as $item) {
4892 - $needed_ids[$item['id']] = true;
4893 - }
4894 - $needed_ids = array_keys($needed_ids);
4895 -
4896 - $content_map = [];
4897 - if (!empty($needed_ids)) {
4898 - foreach (array_chunk($needed_ids, 250) as $chunk_ids) {
4899 - $placeholders = implode(',', array_fill(0, count($chunk_ids), '%d'));
4900 - $rows = $wpdb->get_results($wpdb->prepare(
4901 - "SELECT id, article_content FROM {$system_prompt_table} WHERE id IN ($placeholders)",
4902 - ...$chunk_ids
4903 - ));
4904 - foreach ($rows as $r) {
4905 - $content_map[$r->id] = $r->article_content;
4906 - }
4907 - unset($rows);
4908 - }
4909 - }
4910 -
4911 - // Build the all_similarities display array from the top 10
4912 - $all_similarities = [];
4913 - foreach ($top_display as $item) {
4914 - $article_content_for_parse = $content_map[$item['id']] ?? '';
4915 - $parsed_for_display = MxChat_Chunker::parse_stored_chunk($article_content_for_parse);
4916 - $is_chunk = $parsed_for_display['is_chunked'];
4917 - $chunk_meta = $parsed_for_display['metadata'];
4918 -
4919 - if (!empty($item['source_url']) && $item['source_url'] !== '#') {
4920 - $source_display = $item['source_url'];
4921 - } else {
4922 - $content_preview = strip_tags($article_content_for_parse);
4923 - $content_preview = preg_replace('/\s+/', ' ', $content_preview);
4924 - $source_display = substr(trim($content_preview), 0, 50) . '...';
4925 - }
4926 -
4927 - $all_similarities[] = [
4928 - 'document_id' => $item['id'],
4929 - 'similarity' => $item['similarity'],
4930 - 'similarity_percentage' => round($item['similarity'] * 100, 2),
4931 - 'above_threshold' => $item['similarity'] >= $similarity_threshold,
4932 - 'source_display' => $source_display,
4933 - 'content_preview' => substr(strip_tags($parsed_for_display['text'] ?? ''), 0, 100) . '...',
4934 - 'used_for_context' => false,
4935 - 'role_restriction' => $item['role_restriction'],
4936 - 'has_access' => $item['has_access'],
4937 - 'filtered_out' => !$item['has_access'],
4938 - 'is_chunk' => $is_chunk,
4939 - 'chunk_index' => $is_chunk ? ($chunk_meta['chunk_index'] ?? 0) : null,
4940 - 'total_chunks' => $is_chunk ? ($chunk_meta['total_chunks'] ?? 1) : null
4941 - ];
4942 - }
4943 -
4944 - // Build url_groups from candidates for chunk reassembly
4945 - $url_groups = array();
4946 - foreach ($candidates as $cand) {
4947 - $article_content = $content_map[$cand['id']] ?? '';
4948 - $parsed = MxChat_Chunker::parse_stored_chunk($article_content);
4949 - $is_chunked = $parsed['is_chunked'];
4950 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
4951 - $text_content = $parsed['text'];
4952 -
4953 - $source_url = $cand['source_url'];
4954 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $cand['id'];
4955 -
4956 - if (!isset($url_groups[$group_key])) {
4957 - $url_groups[$group_key] = array(
4958 - 'source_url' => $source_url,
4959 - 'best_score' => 0,
4960 - 'is_chunked' => $is_chunked,
4961 - 'chunks' => array(),
4962 - 'single_text' => '',
4963 - 'single_id' => null
4964 - );
4965 - }
4966 -
4967 - if ($cand['similarity'] > $url_groups[$group_key]['best_score']) {
4968 - $url_groups[$group_key]['best_score'] = $cand['similarity'];
4969 - }
4970 -
4971 - if ($is_chunked) {
4972 - $url_groups[$group_key]['is_chunked'] = true;
4973 - $url_groups[$group_key]['chunks'][] = array(
4974 - 'id' => $cand['id'],
4975 - 'score' => $cand['similarity'],
4976 - 'chunk_index' => $chunk_index,
4977 - 'text' => $text_content
4978 - );
4979 - } else {
4980 - $url_groups[$group_key]['single_text'] = $text_content;
4981 - $url_groups[$group_key]['single_id'] = $cand['id'];
4982 - }
4983 - }
4984 -
4985 4117 // Sort ALL similarities for testing display (highest first)
4986 4118 usort($all_similarities, function ($a, $b) {
4987 4119 return $b['similarity'] <=> $a['similarity'];
4988 4120 });
4989 -
4990 - // Sort URL groups by best score (highest first)
4991 - uasort($url_groups, function($a, $b) {
4992 - return $b['best_score'] <=> $a['best_score'];
4121 +
4122 + // Sort relevant results by similarity (highest first)
4123 + usort($relevant_results, function ($a, $b) {
4124 + return $b['similarity'] <=> $a['similarity'];
4993 4125 });
4994 -
4995 - // Get RAG sources limit from options (default 6, min 3, max 10)
4996 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
4997 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
4998 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
4999 -
5000 - // Take top N unique URLs based on user setting
5001 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5002 -
5003 - // Track which document IDs are used for context
4126 +
4127 + // Get top 5 results for actual content (standard approach)
4128 + $top_results = array_slice($relevant_results, 0, 3);
4129 +
4130 + // NOW mark which documents are actually used for context
5004 4131 $used_document_ids = [];
5005 - foreach ($top_urls as $group) {
5006 - if ($group['is_chunked']) {
5007 - foreach ($group['chunks'] as $chunk) {
5008 - $used_document_ids[] = $chunk['id'];
5009 - }
5010 - } elseif ($group['single_id']) {
5011 - $used_document_ids[] = $group['single_id'];
5012 - }
4132 + foreach ($top_results as $result) {
4133 + $used_document_ids[] = $result['id'];
5013 4134 }
5014 -
4135 +
5015 4136 // Update the all_similarities array to mark which were actually used
5016 4137 foreach ($all_similarities as &$similarity_item) {
5017 4138 $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
5018 4139 }
5019 -
5020 - // Store top 10 for testing panel
4140 +
4141 + // Store top 10 for testing panel (now with correct used_for_context flags and role info)
5021 4142 $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
5022 - $this->last_similarity_analysis['total_checked'] = $total_checked;
5023 -
4143 + $this->last_similarity_analysis['total_checked'] = count($embeddings);
4144 +
5024 4145 // Initialize final content
5025 4146 $content = '';
5026 - $matches_used = 0;
5027 - $total_chunks_used = 0;
5028 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5029 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5030 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5031 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5032 -
5033 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5034 - // Use fresh options to ensure we get the latest setting value
5035 - $fresh_options = get_option('mxchat_options', []);
5036 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5037 -
5038 - // Build content from top sources
5039 - foreach ($top_urls as $group_key => $group) {
5040 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5041 -
5042 - // Stop if we've hit the total chunk limit
5043 - if ($total_chunks_used >= $max_total_chunks) {
5044 - break;
4147 +
4148 + // Track document IDs to avoid duplicates
4149 + $added_document_ids = [];
4150 +
4151 + // Fetch and format content for each selected result
4152 + foreach ($top_results as $index => $result) {
4153 + if (in_array($result['id'], $added_document_ids)) {
4154 + continue;
5045 4155 }
5046 -
5047 - $full_text = '';
5048 - $chunks_in_this_source = 1; // Default for non-chunked content
5049 -
5050 - if ($group['is_chunked']) {
5051 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5052 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5053 -
5054 - // Fetch chunks for this URL with limit
5055 - $full_text = $this->reassemble_chunks_from_wordpress($source_url, $chunks_remaining, $chunks_in_this_source);
5056 -
5057 - // If fetching all chunks fails, fall back to matched chunks
5058 - if (empty($full_text)) {
5059 - // Sort matched chunks by index and concatenate
5060 - usort($group['chunks'], function($a, $b) {
5061 - return $a['chunk_index'] <=> $b['chunk_index'];
5062 - });
5063 -
5064 - $chunk_texts = array();
5065 - $chunks_in_this_source = 0;
5066 - foreach ($group['chunks'] as $chunk) {
5067 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5068 - break;
4156 +
4157 + $chunk_content = $this->fetch_content_with_product_links($result['id']);
4158 + $added_document_ids[] = $result['id'];
4159 +
4160 + // NEW: Extract source_url from database for this result
4161 + $source_url = $wpdb->get_var($wpdb->prepare(
4162 + "SELECT source_url FROM {$system_prompt_table} WHERE id = %d",
4163 + $result['id']
4164 + ));
4165 +
4166 + // NEW: Add source_url to valid URLs list if it exists and is not empty/placeholder
4167 + if (!empty($source_url) && $source_url !== '#') {
4168 + $valid_urls[] = $source_url;
4169 + }
4170 +
4171 + // NEW: Extract any URLs from the article content itself
4172 + preg_match_all(
4173 + '#\bhttps?://[^\s<>"\']+#i',
4174 + $chunk_content,
4175 + $content_urls
4176 + );
4177 + if (!empty($content_urls[0])) {
4178 + $valid_urls = array_merge($valid_urls, $content_urls[0]);
4179 + }
4180 +
4181 + $content .= "## Reference " . ($index + 1) . " ##\n";
4182 + $content .= $chunk_content . "\n\n";
4183 +
4184 + // PDF surrounding pages logic (unchanged)
4185 + if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
4186 + $surrounding_content = $wpdb->get_results($wpdb->prepare(
4187 + "SELECT id, article_content, role_restriction FROM {$system_prompt_table}
4188 + WHERE id IN (
4189 + (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
4190 + (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
4191 + )",
4192 + $result['id'],
4193 + $result['id']
4194 + ));
4195 +
4196 + // Check role access for surrounding content too
4197 + if (!empty($surrounding_content[0])) {
4198 + $surrounding_role = $surrounding_content[0]->role_restriction ?? 'public';
4199 + if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
4200 + // NEW: Extract URLs from surrounding content too
4201 + preg_match_all(
4202 + '#\bhttps?://[^\s<>"\']+#i',
4203 + $surrounding_content[0]->article_content,
4204 + $surrounding_urls
4205 + );
4206 + if (!empty($surrounding_urls[0])) {
4207 + $valid_urls = array_merge($valid_urls, $surrounding_urls[0]);
5069 4208 }
5070 - $chunk_texts[] = $chunk['text'];
5071 - $chunks_in_this_source++;
4209 +
4210 + $content .= "## Related Content ##\n";
4211 + $content .= $surrounding_content[0]->article_content . "\n\n";
4212 + $added_document_ids[] = $surrounding_content[0]->id;
5072 4213 }
5073 - $full_text = implode("\n\n", $chunk_texts);
5074 4214 }
5075 - } else {
5076 - $full_text = $group['single_text'];
5077 - $chunks_in_this_source = 1;
5078 - }
5079 -
5080 - if (!empty($full_text)) {
5081 - // Strip URLs from content if citation links are disabled
5082 - if (!$citation_links_enabled) {
5083 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5084 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
5085 - }
5086 -
5087 - // Use numbered reference for URL-based entries, plain info label for manual entries
5088 - // Manual entries are stored with an internal mxchat:// placeholder URL — never expose them as citations
5089 - if (!empty($source_url) && $source_url !== '#' && strpos($source_url, 'mxchat://') !== 0) {
5090 - $matches_used++;
5091 - $content .= "## Reference " . $matches_used . " ##\n";
5092 - $content .= $full_text . "\n\n";
5093 -
5094 - // Only include citation URLs if citation links are enabled
5095 - if ($citation_links_enabled) {
5096 - $valid_urls[] = $source_url;
5097 - $content .= "URL: " . $source_url . "\n\n";
4215 +
4216 + if (!empty($surrounding_content[1])) {
4217 + $surrounding_role = $surrounding_content[1]->role_restriction ?? 'public';
4218 + if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
4219 + // NEW: Extract URLs from surrounding content too
4220 + preg_match_all(
4221 + '#\bhttps?://[^\s<>"\']+#i',
4222 + $surrounding_content[1]->article_content,
4223 + $surrounding_urls
4224 + );
4225 + if (!empty($surrounding_urls[0])) {
4226 + $valid_urls = array_merge($valid_urls, $surrounding_urls[0]);
4227 + }
4228 +
4229 + $content .= "## Related Content ##\n";
4230 + $content .= $surrounding_content[1]->article_content . "\n\n";
4231 + $added_document_ids[] = $surrounding_content[1]->id;
5098 4232 }
5099 - } else {
5100 - // Manual entry — no reference number, no citation
5101 - $content .= "## Information ##\n";
5102 - $content .= $full_text . "\n\n";
5103 4233 }
5104 -
5105 - // Extract any URLs from the text content itself (only if citation links enabled)
5106 - if ($citation_links_enabled) {
5107 - preg_match_all(
5108 - '#\bhttps?://[^\s<>"\']+#i',
5109 - $full_text,
5110 - $content_urls
5111 - );
5112 - if (!empty($content_urls[0])) {
5113 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5114 - }
5115 - }
5116 -
5117 - $total_chunks_used += $chunks_in_this_source;
5118 4234 }
5119 4235 }
5120 -
4236 +
5121 4237 // NEW: Store unique valid URLs for validation
5122 4238 $this->current_valid_urls = array_unique($valid_urls);
5123 -
5124 - // Store sources and chunks counts for testing/transcript display
5125 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5126 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5127 -
5128 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5129 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5130 -
4239 +
5131 4240 // Add response guidelines
5132 - if (empty($top_urls)) {
4241 + if (empty($top_results)) {
5133 4242 $content = "No reference information was found for this query.\n\n";
5134 4243 } else {
5135 - // Build response guidelines based on citation links setting
5136 4244 $content .= "\n## Response Guidelines ##\n" .
5137 4245 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5138 4246 "Be conversational and friendly, but never mention your knowledge base or training data. " .
5139 4247 "If you don't have specific information or are uncertain about any details, it's always " .
5140 4248 "better to honestly say you don't know rather than making up or guessing at answers. " .
5141 - "When information is incomplete, let them know you are unsure.\n\n";
5142 -
5143 - // Only add hyperlink instructions if citation links are enabled
5144 - if ($citation_links_enabled) {
5145 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5146 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5147 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
5148 - } else {
5149 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5150 - "Simply provide helpful answers based on the reference information without citing sources.";
5151 - }
4249 + "When information is incomplete, let them know you are unsure.\n\n" .
4250 + "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
4251 + "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
5152 4252 }
5153 4253
5154 4254 return trim($content);
5155 4255 }
5156 4256
5157 -/**
5158 - * Fetch and reassemble chunks for a URL from WordPress database
5159 - *
5160 - * @param string $source_url The source URL to fetch chunks for
5161 - * @param int $max_chunks Maximum number of chunks to return (0 = unlimited)
5162 - * @param int &$chunk_count Reference to store the actual number of chunks returned
5163 - * @return string Reassembled content from chunks
5164 - */
5165 -private function reassemble_chunks_from_wordpress($source_url, $max_chunks = 0, &$chunk_count = 0) {
5166 - global $wpdb;
5167 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
5168 -
5169 - // Fetch all rows with this source_url
5170 - $rows = $wpdb->get_results($wpdb->prepare(
5171 - "SELECT article_content FROM {$table}
5172 - WHERE source_url = %s
5173 - ORDER BY id ASC",
5174 - $source_url
5175 - ));
5176 -
5177 - if (empty($rows)) {
5178 - $chunk_count = 0;
5179 - return '';
5180 - }
5181 -
5182 - // Parse and sort chunks by index
5183 - $chunks = array();
5184 - foreach ($rows as $row) {
5185 - $parsed = MxChat_Chunker::parse_stored_chunk($row->article_content);
5186 -
5187 - if ($parsed['is_chunked']) {
5188 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
5189 - $chunks[$chunk_index] = $parsed['text'];
5190 - } else {
5191 - // Non-chunked content - just return it
5192 - $chunks[] = $parsed['text'];
5193 - }
5194 - }
5195 -
5196 - // Sort by chunk index
5197 - ksort($chunks);
5198 -
5199 - // Apply chunk limit if specified
5200 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5201 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5202 - }
5203 -
5204 - // Store actual chunk count
5205 - $chunk_count = count($chunks);
5206 -
5207 - // Reassemble content
5208 - return implode("\n\n", $chunks);
5209 -}
5210 -
5211 4257 private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
5212 4258 global $wpdb;
5213 4259
5214 - //error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
5215 - //error_log(" - bot_id: " . $bot_id);
5216 - //error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
5217 - //error_log(" - user_embedding count: " . (is_array($user_embedding) ? count($user_embedding) : 'N/A'));
4260 + error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
4261 + error_log(" - bot_id: " . $bot_id);
4262 + error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
4263 + error_log(" - user_embedding count: " . (is_array($user_embedding) ? count($user_embedding) : 'N/A'));
5218 4264
5219 4265 // Use bot-specific config or fall back to default
5220 4266 if ($bot_config === null) {
5221 4267 $bot_config = $this->get_bot_pinecone_config($bot_id);
@@ -5224,12 +4270,12 @@
5224 4270 $api_key = $bot_config['api_key'] ?? '';
5225 4271 $host = $bot_config['host'] ?? '';
5226 4272 $namespace = $bot_config['namespace'] ?? '';
5227 4273
5228 - //error_log("MXCHAT DEBUG: Pinecone query parameters:");
5229 - //error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
5230 - //error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
5231 - //error_log(" - Namespace: " . (empty($namespace) ? 'EMPTY (will use default)' : $namespace));
4274 + error_log("MXCHAT DEBUG: Pinecone query parameters:");
4275 + error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
4276 + error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
4277 + error_log(" - Namespace: " . (empty($namespace) ? 'EMPTY (will use default)' : $namespace));
5232 4278
5233 4279 // Initialize similarity analysis storage
5234 4280 $this->last_similarity_analysis = [
5235 4281 'knowledge_base_type' => 'Pinecone',
@@ -5243,11 +4289,11 @@
5243 4289 // NEW: Initialize valid URLs array
5244 4290 $valid_urls = [];
5245 4291
5246 4292 if (empty($host) || empty($api_key)) {
5247 - //error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
5248 - //error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
5249 - //error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
4293 + error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
4294 + error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
4295 + error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
5250 4296 // Store empty array for valid URLs since we can't proceed
5251 4297 $this->current_valid_urls = [];
5252 4298 return '';
5253 4299 }
@@ -5269,9 +4315,9 @@
5269 4315 $api_endpoint = "https://{$host}/query";
5270 4316
5271 4317 $request_body = array(
5272 4318 'vector' => $user_embedding,
5273 - 'topK' => 50, // Increased for chunked content grouping - need more candidates to find top N unique URLs
4319 + 'topK' => 20, // Request more to get good testing data
5274 4320 'includeMetadata' => true,
5275 4321 'includeValues' => true
5276 4322 );
5277 4323
@@ -5279,11 +4325,11 @@
5279 4325 if (!empty($namespace)) {
5280 4326 $request_body['namespace'] = $namespace;
5281 4327 }
5282 4328
5283 - //error_log("MXCHAT DEBUG: About to call Pinecone API");
5284 - //error_log(" - Endpoint: " . $api_endpoint);
5285 - //error_log(" - Namespace in request: " . (!empty($namespace) ? $namespace : 'NOT SET'));
4329 + error_log("MXCHAT DEBUG: About to call Pinecone API");
4330 + error_log(" - Endpoint: " . $api_endpoint);
4331 + error_log(" - Namespace in request: " . (!empty($namespace) ? $namespace : 'NOT SET'));
5286 4332
5287 4333 $response = wp_remote_post($api_endpoint, array(
5288 4334 'headers' => array(
5289 4335 'Api-Key' => $api_key,
@@ -5294,9 +4340,9 @@
5294 4340 'timeout' => 30
5295 4341 ));
5296 4342
5297 4343 if (is_wp_error($response)) {
5298 - //error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
4344 + error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
5299 4345 // Store empty array for valid URLs
5300 4346 $this->current_valid_urls = [];
5301 4347 return '';
5302 4348 }
@@ -5301,13 +4347,13 @@
5301 4347 return '';
5302 4348 }
5303 4349
5304 4350 $response_code = wp_remote_retrieve_response_code($response);
5305 - //error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
4351 + error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
5306 4352
5307 4353 if ($response_code !== 200) {
5308 4354 $response_body = wp_remote_retrieve_body($response);
5309 - //error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
4355 + error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
5310 4356 // Store empty array for valid URLs
5311 4357 $this->current_valid_urls = [];
5312 4358 return '';
5313 4359 }
@@ -5313,42 +4359,42 @@
5313 4359 }
5314 4360
5315 4361 // ADD DETAILED DEBUG SECTION HERE
5316 4362 $response_body = wp_remote_retrieve_body($response);
5317 - //error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
4363 + error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
5318 4364
5319 4365 $results = json_decode($response_body, true);
5320 4366
5321 4367 if (json_last_error() !== JSON_ERROR_NONE) {
5322 - //error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
5323 - //error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
4368 + error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
4369 + error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
5324 4370 // Store empty array for valid URLs
5325 4371 $this->current_valid_urls = [];
5326 4372 return '';
5327 4373 }
5328 4374
5329 - //error_log("MXCHAT DEBUG: Pinecone response structure:");
5330 - //error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
5331 - //error_log(" - Has 'namespace' key: " . (isset($results['namespace']) ? 'yes (' . $results['namespace'] . ')' : 'no'));
4375 + error_log("MXCHAT DEBUG: Pinecone response structure:");
4376 + error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
4377 + error_log(" - Has 'namespace' key: " . (isset($results['namespace']) ? 'yes (' . $results['namespace'] . ')' : 'no'));
5332 4378
5333 4379 if (empty($results['matches'])) {
5334 - //error_log("MXCHAT DEBUG: No matches found in Pinecone response");
5335 - //error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
4380 + error_log("MXCHAT DEBUG: No matches found in Pinecone response");
4381 + error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
5336 4382 // Store empty array for valid URLs
5337 4383 $this->current_valid_urls = [];
5338 4384 return '';
5339 4385 }
5340 4386
5341 - //error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
4387 + error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
5342 4388
5343 4389 // Log first match details for debugging
5344 4390 if (!empty($results['matches'][0])) {
5345 4391 $first_match = $results['matches'][0];
5346 - //error_log("MXCHAT DEBUG: First match details:");
5347 - //error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
5348 - //error_log(" - Has metadata: " . (isset($first_match['metadata']) ? 'yes' : 'no'));
4392 + error_log("MXCHAT DEBUG: First match details:");
4393 + error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
4394 + error_log(" - Has metadata: " . (isset($first_match['metadata']) ? 'yes' : 'no'));
5349 4395 if (isset($first_match['metadata'])) {
5350 - //error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
4396 + error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
5351 4397 }
5352 4398 }
5353 4399
5354 4400 // Initialize the final content
@@ -5354,182 +4400,57 @@
5354 4400 // Initialize the final content
5355 4401 $content = '';
5356 4402 $matches_used = 0;
5357 4403 $matches_used_for_context = [];
5358 - $total_chunks_used = 0;
5359 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5360 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5361 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5362 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5363 -
5364 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5365 - // Use fresh options to ensure we get the latest setting value
5366 - $fresh_options = get_option('mxchat_options', []);
5367 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5368 -
5369 - // NEW CHUNKING LOGIC: Group results by source_url for chunk reassembly
5370 - $url_groups = array();
5371 -
4404 +
4405 + // Process each match for actual content generation (lazy role checking)
5372 4406 foreach ($results['matches'] as $index => $match) {
5373 4407 // Skip if similarity is below threshold
5374 4408 if ($match['score'] < $similarity_threshold) {
5375 4409 continue;
5376 4410 }
5377 -
5378 - $metadata = $match['metadata'] ?? array();
5379 - $source_url = $metadata['source_url'] ?? '';
5380 - $match_id = $match['id'] ?? '';
5381 -
5382 - // LAZY ROLE CHECK: Only check role for content we're actually considering
5383 - $role_restriction = $this->get_single_vector_role($match_id, $metadata);
5384 - $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
5385 -
5386 - // Skip if user doesn't have access
5387 - if (!$has_access) {
5388 - continue;
5389 - }
5390 -
5391 - // Use a unique key for manual entries without a source URL
5392 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $match_id;
5393 -
5394 - // Group by source URL (or unique key for manual entries)
5395 - if (!isset($url_groups[$group_key])) {
5396 - $url_groups[$group_key] = array(
5397 - 'source_url' => $source_url,
5398 - 'best_score' => 0,
5399 - 'is_chunked' => isset($metadata['is_chunked']) && $metadata['is_chunked'],
5400 - 'chunks' => array(),
5401 - 'single_text' => ''
5402 - );
5403 - }
5404 -
5405 - // Track best score for this group
5406 - if ($match['score'] > $url_groups[$group_key]['best_score']) {
5407 - $url_groups[$group_key]['best_score'] = $match['score'];
5408 - }
5409 -
5410 - // Store chunk info or single text
5411 - if ($url_groups[$group_key]['is_chunked']) {
5412 - $url_groups[$group_key]['chunks'][] = array(
5413 - 'id' => $match_id,
5414 - 'score' => $match['score'],
5415 - 'chunk_index' => $metadata['chunk_index'] ?? 0,
5416 - 'text' => $metadata['text'] ?? ''
5417 - );
5418 - } else {
5419 - // Non-chunked content - just store the text
5420 - $url_groups[$group_key]['single_text'] = $metadata['text'] ?? '';
5421 - $url_groups[$group_key]['single_id'] = $match_id;
5422 - }
5423 - }
5424 -
5425 - // Sort URL groups by best score (highest first)
5426 - uasort($url_groups, function($a, $b) {
5427 - return $b['best_score'] <=> $a['best_score'];
5428 - });
5429 -
5430 - // Get RAG sources limit from options (default 6, min 3, max 10)
5431 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
5432 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
5433 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
5434 -
5435 - // Take top N unique URLs based on user setting
5436 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5437 -
5438 - // Track which match IDs are actually used for context
5439 - foreach ($top_urls as $group) {
5440 - if ($group['is_chunked']) {
5441 - foreach ($group['chunks'] as $chunk) {
5442 - $matches_used_for_context[] = $chunk['id'];
5443 - }
5444 - } elseif (!empty($group['single_id'])) {
5445 - $matches_used_for_context[] = $group['single_id'];
5446 - }
5447 - }
5448 -
5449 - // Build content from top sources
5450 - foreach ($top_urls as $group_key => $group) {
5451 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5452 -
5453 - // Stop if we've hit the total chunk limit
5454 - if ($total_chunks_used >= $max_total_chunks) {
4411 +
4412 + // Limit to top 3 matches above threshold
4413 + if ($matches_used >= 3) {
5455 4414 break;
5456 4415 }
5457 -
5458 - $full_text = '';
5459 - $chunks_in_this_source = 1; // Default for non-chunked content
5460 -
5461 - if ($group['is_chunked']) {
5462 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5463 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5464 -
5465 - // Fetch chunks for this URL with limit
5466 - $full_text = $this->reassemble_chunks_from_pinecone($source_url, $bot_config, $chunks_remaining, $chunks_in_this_source);
5467 -
5468 - // If fetching all chunks fails, fall back to matched chunks
5469 - if (empty($full_text)) {
5470 - // Sort matched chunks by index and concatenate
5471 - usort($group['chunks'], function($a, $b) {
5472 - return $a['chunk_index'] <=> $b['chunk_index'];
5473 - });
5474 -
5475 - $chunk_texts = array();
5476 - $chunks_in_this_source = 0;
5477 - foreach ($group['chunks'] as $chunk) {
5478 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5479 - break;
5480 - }
5481 - $chunk_texts[] = $chunk['text'];
5482 - $chunks_in_this_source++;
5483 - }
5484 - $full_text = implode("\n\n", $chunk_texts);
4416 +
4417 + if (!empty($match['metadata']['text'])) {
4418 + // LAZY ROLE CHECK: Only check role for content we're actually considering
4419 + $match_id = $match['id'] ?? '';
4420 + $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
4421 + $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
4422 +
4423 + // Skip if user doesn't have access
4424 + if (!$has_access) {
4425 + continue;
5485 4426 }
5486 - } else {
5487 - $full_text = $group['single_text'];
5488 - $chunks_in_this_source = 1;
5489 - }
5490 -
5491 - if (!empty($full_text)) {
5492 - // Strip URLs from content if citation links are disabled
5493 - if (!$citation_links_enabled) {
5494 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5495 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
4427 +
4428 + // User has access - add to content
4429 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
4430 + $content .= $match['metadata']['text'] . "\n\n";
4431 +
4432 + // NEW: Extract source_url from metadata if it exists
4433 + if (!empty($match['metadata']['source_url']) && $match['metadata']['source_url'] !== '#') {
4434 + $valid_urls[] = $match['metadata']['source_url'];
4435 + $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
5496 4436 }
5497 -
5498 - // Use numbered reference for URL-based entries, plain info label for manual entries
5499 - // Manual entries are stored with an internal mxchat:// placeholder URL — never expose them as citations
5500 - if (!empty($source_url) && $source_url !== '#' && strpos($source_url, 'mxchat://') !== 0) {
5501 - $matches_used++;
5502 - $content .= "## Reference " . $matches_used . " ##\n";
5503 - $content .= $full_text . "\n\n";
5504 -
5505 - // Only include citation URLs if citation links are enabled
5506 - if ($citation_links_enabled) {
5507 - $valid_urls[] = $source_url;
5508 - $content .= "URL: " . $source_url . "\n\n";
5509 - }
5510 - } else {
5511 - // Manual entry — no reference number, no citation
5512 - $content .= "## Information ##\n";
5513 - $content .= $full_text . "\n\n";
4437 +
4438 + // NEW: Extract any URLs from the text content itself
4439 + preg_match_all(
4440 + '#\bhttps?://[^\s<>"\']+#i',
4441 + $match['metadata']['text'],
4442 + $content_urls
4443 + );
4444 + if (!empty($content_urls[0])) {
4445 + $valid_urls = array_merge($valid_urls, $content_urls[0]);
5514 4446 }
5515 -
5516 - // Extract any URLs from the text content itself (only if citation links enabled)
5517 - if ($citation_links_enabled) {
5518 - preg_match_all(
5519 - '#\bhttps?://[^\s<>"\']+#i',
5520 - $full_text,
5521 - $content_urls
5522 - );
5523 - if (!empty($content_urls[0])) {
5524 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5525 - }
5526 - }
5527 -
5528 - $total_chunks_used += $chunks_in_this_source;
4447 +
4448 + $matches_used_for_context[] = $match['id'] ?? $index;
4449 + $matches_used++;
5529 4450 }
5530 4451 }
5531 -
4452 +
5532 4453 // Process ALL matches for testing data (top 10) - with role checking for testing display
5533 4454 $all_matches = [];
5534 4455 foreach ($results['matches'] as $index => $match) {
5535 4456 if ($index >= 10) break; // Limit to top 10 for testing
@@ -5549,19 +4470,9 @@
5549 4470 $source_display = substr(trim($content_preview), 0, 50) . '...';
5550 4471 }
5551 4472
5552 4473 $match_id_for_display = $match['id'] ?? $index;
5553 -
5554 - // Check for chunk metadata in Pinecone
5555 - $is_chunk = isset($match['metadata']['is_chunked']) && $match['metadata']['is_chunked'];
5556 - $chunk_index = isset($match['metadata']['chunk_index']) ? intval($match['metadata']['chunk_index']) : null;
5557 - $total_chunks = isset($match['metadata']['total_chunks']) ? intval($match['metadata']['total_chunks']) : null;
5558 -
5559 - // Also detect chunk from vector ID pattern: {hash}_chunk_{index}
5560 - if (!$is_chunk && MxChat_Chunker::is_chunk_vector_id($match_id_for_display)) {
5561 - $is_chunk = true;
5562 - }
5563 -
4474 +
5564 4475 $all_matches[] = [
5565 4476 'document_id' => $match_id_for_display,
5566 4477 'similarity' => $match['score'],
5567 4478 'similarity_percentage' => round($match['score'] * 100, 2),
@@ -5570,12 +4481,9 @@
5570 4481 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
5571 4482 'used_for_context' => in_array($match_id_for_display, $matches_used_for_context),
5572 4483 'role_restriction' => $role_restriction,
5573 4484 'has_access' => $has_access,
5574 - 'filtered_out' => !$has_access,
5575 - 'is_chunk' => $is_chunk,
5576 - 'chunk_index' => $chunk_index,
5577 - 'total_chunks' => $total_chunks
4485 + 'filtered_out' => !$has_access
5578 4486 ];
5579 4487 }
5580 4488
5581 4489 // Store for testing panel
@@ -5580,40 +4488,26 @@
5580 4488
5581 4489 // Store for testing panel
5582 4490 $this->last_similarity_analysis['top_matches'] = $all_matches;
5583 4491 $this->last_similarity_analysis['total_checked'] = count($results['matches']);
5584 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5585 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5586 -
4492 +
5587 4493 // NEW: Store unique valid URLs for validation
5588 4494 $this->current_valid_urls = array_unique($valid_urls);
5589 -
5590 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5591 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5592 -
4495 +
5593 4496 // Add response guidelines
5594 4497 if ($matches_used === 0) {
5595 4498 $content = "No reference information was found for this query.\n\n";
5596 4499 } else {
5597 - // Build response guidelines based on citation links setting
5598 4500 $content .= "\n## Response Guidelines ##\n" .
5599 4501 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5600 4502 "Be conversational and friendly, but never mention your knowledge base or training data. " .
5601 4503 "If you don't have specific information or are uncertain about any details, it's always " .
5602 4504 "better to honestly say you don't know rather than making up or guessing at answers. " .
5603 - "When information is incomplete, let them know you are unsure.\n\n";
5604 -
5605 - // Only add hyperlink instructions if citation links are enabled
5606 - if ($citation_links_enabled) {
5607 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5608 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5609 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
5610 - } else {
5611 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5612 - "Simply provide helpful answers based on the reference information without citing sources.";
5613 - }
4505 + "When information is incomplete, let them know you are unsure.\n\n" .
4506 + "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
4507 + "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
5614 4508 }
5615 -
4509 +
5616 4510 return trim($content);
5617 4511 }
5618 4512
5619 4513 /**
@@ -5653,518 +4547,12 @@
5653 4547 }
5654 4548
5655 4549 // Cache individual role for 1 hour
5656 4550 wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
5657 -
4551 +
5658 4552 return $role_restriction;
5659 4553 }
5660 4554
5661 -/**
5662 - * Fetch and reassemble all chunks for a URL from Pinecone
5663 - *
5664 - * @param string $source_url The source URL to fetch chunks for
5665 - * @param array $bot_config Bot-specific Pinecone configuration
5666 - * @return string Reassembled content from all chunks
5667 - */
5668 -private function reassemble_chunks_from_pinecone($source_url, $bot_config, $max_chunks = 0, &$chunk_count = 0) {
5669 - $api_key = $bot_config['api_key'] ?? '';
5670 - $host = $bot_config['host'] ?? '';
5671 - $namespace = $bot_config['namespace'] ?? '';
5672 -
5673 - if (empty($host) || empty($api_key)) {
5674 - $chunk_count = 0;
5675 - return '';
5676 - }
5677 -
5678 - $base_hash = md5($source_url);
5679 -
5680 - // Use Pinecone list API to find all chunk vectors with this prefix
5681 - $list_url = "https://{$host}/vectors/list";
5682 -
5683 - // Limit to max_chunks if specified, otherwise fetch up to 100
5684 - $fetch_limit = ($max_chunks > 0 && $max_chunks < 100) ? $max_chunks : 100;
5685 -
5686 - $list_body = array(
5687 - 'prefix' => $base_hash . '_chunk_',
5688 - 'limit' => $fetch_limit
5689 - );
5690 -
5691 - if (!empty($namespace)) {
5692 - $list_body['namespace'] = $namespace;
5693 - }
5694 -
5695 - $list_response = wp_remote_post($list_url, array(
5696 - 'headers' => array(
5697 - 'Api-Key' => $api_key,
5698 - 'accept' => 'application/json',
5699 - 'content-type' => 'application/json'
5700 - ),
5701 - 'body' => wp_json_encode($list_body),
5702 - 'timeout' => 30
5703 - ));
5704 -
5705 - if (is_wp_error($list_response)) {
5706 - //error_log('[MXCHAT-CHUNK] List API error: ' . $list_response->get_error_message());
5707 - return '';
5708 - }
5709 -
5710 - $list_data = json_decode(wp_remote_retrieve_body($list_response), true);
5711 -
5712 - if (empty($list_data['vectors'])) {
5713 - //error_log('[MXCHAT-CHUNK] No chunk vectors found for URL: ' . $source_url);
5714 - return '';
5715 - }
5716 -
5717 - // Extract vector IDs
5718 - $vector_ids = array();
5719 - foreach ($list_data['vectors'] as $vector) {
5720 - if (isset($vector['id'])) {
5721 - $vector_ids[] = $vector['id'];
5722 - }
5723 - }
5724 -
5725 - if (empty($vector_ids)) {
5726 - return '';
5727 - }
5728 -
5729 - // Fetch all chunk content
5730 - $fetch_url = "https://{$host}/vectors/fetch";
5731 -
5732 - $fetch_body = array(
5733 - 'ids' => $vector_ids
5734 - );
5735 -
5736 - if (!empty($namespace)) {
5737 - $fetch_body['namespace'] = $namespace;
5738 - }
5739 -
5740 - $fetch_response = wp_remote_post($fetch_url, array(
5741 - 'headers' => array(
5742 - 'Api-Key' => $api_key,
5743 - 'accept' => 'application/json',
5744 - 'content-type' => 'application/json'
5745 - ),
5746 - 'body' => wp_json_encode($fetch_body),
5747 - 'timeout' => 30
5748 - ));
5749 -
5750 - if (is_wp_error($fetch_response)) {
5751 - //error_log('[MXCHAT-CHUNK] Fetch API error: ' . $fetch_response->get_error_message());
5752 - return '';
5753 - }
5754 -
5755 - $fetch_data = json_decode(wp_remote_retrieve_body($fetch_response), true);
5756 -
5757 - if (empty($fetch_data['vectors'])) {
5758 - return '';
5759 - }
5760 -
5761 - // Sort chunks by index and reassemble
5762 - $chunks = array();
5763 - foreach ($fetch_data['vectors'] as $id => $vector) {
5764 - $metadata = $vector['metadata'] ?? array();
5765 - $chunk_index = $metadata['chunk_index'] ?? 0;
5766 - $text = $metadata['text'] ?? '';
5767 -
5768 - // Store chunk with its index
5769 - $chunks[$chunk_index] = $text;
5770 - }
5771 -
5772 - // Sort by chunk index
5773 - ksort($chunks);
5774 -
5775 - // Apply chunk limit if specified
5776 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5777 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5778 - }
5779 -
5780 - // Store actual chunk count
5781 - $chunk_count = count($chunks);
5782 -
5783 - // Reassemble content
5784 - return implode("\n\n", $chunks);
5785 -}
5786 -
5787 -/**
5788 - * Search for relevant content using OpenAI Vector Store (File Search)
5789 - *
5790 - * @param string $user_query The user's query text
5791 - * @param string $bot_id The bot ID
5792 - * @param array $vectorstore_config Vector Store configuration
5793 - * @return string Formatted context string with references
5794 - */
5795 -private function find_relevant_content_openai_vectorstore($user_query, $bot_id = 'default', $vectorstore_config = array()) {
5796 - //error_log("MXCHAT DEBUG: find_relevant_content_openai_vectorstore called");
5797 - //error_log(" - bot_id: " . $bot_id);
5798 - //error_log(" - user_query length: " . strlen($user_query));
5799 -
5800 - // Get OpenAI API key
5801 - $mxchat_options = get_option('mxchat_options', array());
5802 - $api_key = $mxchat_options['api_key'] ?? '';
5803 -
5804 - // Reset vectorstore error tracking
5805 - $this->last_vectorstore_error = null;
5806 -
5807 - if (empty($api_key)) {
5808 - //error_log("MXCHAT DEBUG ERROR: OpenAI API key not configured");
5809 - $this->last_vectorstore_error = 'Vector Store search failed: OpenAI API key is not configured.';
5810 - $this->current_valid_urls = [];
5811 - return '';
5812 - }
5813 -
5814 - // Get Vector Store configuration
5815 - if (empty($vectorstore_config)) {
5816 - $vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
5817 - }
5818 -
5819 - $vectorstore_ids_string = $vectorstore_config['vectorstore_ids'] ?? '';
5820 - $max_results = $vectorstore_config['max_results'] ?? 5;
5821 -
5822 - if (empty($vectorstore_ids_string)) {
5823 - //error_log("MXCHAT DEBUG ERROR: No Vector Store IDs configured");
5824 - $this->last_vectorstore_error = 'Vector Store search failed: No Vector Store IDs are configured for this bot.';
5825 - $this->current_valid_urls = [];
5826 - return '';
5827 - }
5828 -
5829 - // Parse Vector Store IDs
5830 - $vectorstore_ids = array_map('trim', explode(',', $vectorstore_ids_string));
5831 - $vectorstore_ids = array_filter($vectorstore_ids); // Remove empty values
5832 -
5833 - //error_log("MXCHAT DEBUG: Vector Store IDs: " . implode(', ', $vectorstore_ids));
5834 - //error_log("MXCHAT DEBUG: Max results: " . $max_results);
5835 -
5836 - // Initialize similarity analysis storage
5837 - $this->last_similarity_analysis = [
5838 - 'knowledge_base_type' => 'OpenAI Vector Store',
5839 - 'bot_id' => $bot_id,
5840 - 'vectorstore_ids' => $vectorstore_ids,
5841 - 'top_matches' => [],
5842 - 'threshold_used' => 0,
5843 - 'total_checked' => 0
5844 - ];
5845 -
5846 - $valid_urls = [];
5847 -
5848 - // Get the selected model
5849 - $bot_options = $this->get_bot_options($bot_id);
5850 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
5851 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
5852 -
5853 - // Verify it's an OpenAI model
5854 - if (!$this->is_openai_chat_model($selected_model)) {
5855 - //error_log("MXCHAT DEBUG ERROR: Vector Store search requires OpenAI model. Current: " . $selected_model);
5856 - $this->last_vectorstore_error = 'Vector Store search requires an OpenAI model. Current model: ' . $selected_model;
5857 - $this->current_valid_urls = [];
5858 - return '';
5859 - }
5860 -
5861 - // Use OpenAI Responses API with file_search tool
5862 - $request_body = array(
5863 - 'model' => $selected_model,
5864 - 'input' => $user_query,
5865 - 'tools' => array(
5866 - array(
5867 - 'type' => 'file_search',
5868 - 'vector_store_ids' => $vectorstore_ids,
5869 - 'max_num_results' => intval($max_results)
5870 - )
5871 - ),
5872 - 'include' => array('output[*].file_search_call.search_results')
5873 - );
5874 -
5875 - //error_log("MXCHAT VECTORSTORE: ========== REQUEST START ==========");
5876 - //error_log("MXCHAT VECTORSTORE: Model: " . $selected_model);
5877 - //error_log("MXCHAT VECTORSTORE: Query: " . substr($user_query, 0, 200));
5878 - //error_log("MXCHAT VECTORSTORE: Vector Store IDs: " . implode(', ', $vectorstore_ids));
5879 - //error_log("MXCHAT VECTORSTORE: Max Results: " . $max_results);
5880 - //error_log("MXCHAT VECTORSTORE: Request body: " . wp_json_encode($request_body));
5881 -
5882 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
5883 - 'headers' => array(
5884 - 'Authorization' => 'Bearer ' . $api_key,
5885 - 'Content-Type' => 'application/json'
5886 - ),
5887 - 'body' => wp_json_encode($request_body),
5888 - 'timeout' => 60
5889 - ));
5890 -
5891 - if (is_wp_error($response)) {
5892 - //error_log("MXCHAT VECTORSTORE ERROR: WP Error: " . $response->get_error_message());
5893 - $this->last_vectorstore_error = 'Vector Store API request failed: ' . $response->get_error_message();
5894 - $this->current_valid_urls = [];
5895 - return '';
5896 - }
5897 -
5898 - $response_code = wp_remote_retrieve_response_code($response);
5899 - //error_log("MXCHAT VECTORSTORE: Response code: " . $response_code);
5900 -
5901 - $response_body = wp_remote_retrieve_body($response);
5902 - //error_log("MXCHAT VECTORSTORE: Raw response (first 2000 chars): " . substr($response_body, 0, 2000));
5903 -
5904 - if ($response_code !== 200) {
5905 - //error_log("MXCHAT VECTORSTORE ERROR: API error response: " . $response_body);
5906 - $api_error_detail = '';
5907 - $decoded_error = json_decode($response_body, true);
5908 - if (isset($decoded_error['error']['message'])) {
5909 - $api_error_detail = $decoded_error['error']['message'];
5910 - }
5911 - $this->last_vectorstore_error = 'Vector Store API returned HTTP ' . $response_code . ($api_error_detail ? ': ' . $api_error_detail : '');
5912 - $this->current_valid_urls = [];
5913 - return '';
5914 - }
5915 - $result = json_decode($response_body, true);
5916 -
5917 - if (json_last_error() !== JSON_ERROR_NONE) {
5918 - //error_log("MXCHAT VECTORSTORE ERROR: JSON decode error: " . json_last_error_msg());
5919 - $this->last_vectorstore_error = 'Vector Store response could not be parsed: ' . json_last_error_msg();
5920 - $this->current_valid_urls = [];
5921 - return '';
5922 - }
5923 -
5924 - // Debug: Log the structure of the result
5925 - //error_log("MXCHAT VECTORSTORE: Result keys: " . implode(', ', array_keys($result)));
5926 - if (isset($result['output'])) {
5927 - //error_log("MXCHAT VECTORSTORE: Output count: " . count($result['output']));
5928 - foreach ($result['output'] as $idx => $out) {
5929 - //error_log("MXCHAT VECTORSTORE: Output[$idx] type: " . ($out['type'] ?? 'unknown'));
5930 - //error_log("MXCHAT VECTORSTORE: Output[$idx] keys: " . implode(', ', array_keys($out)));
5931 - }
5932 - } else {
5933 - //error_log("MXCHAT VECTORSTORE: No 'output' key in result!");
5934 - }
5935 -
5936 - // Extract file search results from the response
5937 - $content = '';
5938 - $matches_used = 0;
5939 - $all_matches = [];
5940 -
5941 - // The Responses API returns output array with tool results
5942 - if (isset($result['output']) && is_array($result['output'])) {
5943 - foreach ($result['output'] as $output_item) {
5944 - // Look for file_search_call results
5945 - if (isset($output_item['type']) && $output_item['type'] === 'file_search_call') {
5946 - //error_log("MXCHAT VECTORSTORE: Found file_search_call output item");
5947 - //error_log("MXCHAT VECTORSTORE: file_search_call keys: " . implode(', ', array_keys($output_item)));
5948 -
5949 - // Check for search_results in the output item directly
5950 - $search_results = $output_item['search_results'] ?? $output_item['results'] ?? [];
5951 - //error_log("MXCHAT VECTORSTORE: Search results count: " . count($search_results));
5952 -
5953 - if (empty($search_results)) {
5954 - //error_log("MXCHAT VECTORSTORE: No search results found in file_search_call");
5955 - //error_log("MXCHAT VECTORSTORE: file_search_call content: " . wp_json_encode($output_item));
5956 - }
5957 -
5958 - foreach ($search_results as $index => $search_result) {
5959 - $filename = $search_result['filename'] ?? '';
5960 - $score = $search_result['score'] ?? 0;
5961 - $text_content = '';
5962 -
5963 - // Extract text content from the result
5964 - // The text can be directly on the result OR nested under content array
5965 - if (isset($search_result['text']) && !empty($search_result['text'])) {
5966 - // Direct text field (OpenAI's actual format)
5967 - $text_content = $search_result['text'];
5968 - //error_log("MXCHAT VECTORSTORE: Found text directly on result[$index], length: " . strlen($text_content));
5969 - } elseif (isset($search_result['content']) && is_array($search_result['content'])) {
5970 - // Nested content array format
5971 - foreach ($search_result['content'] as $content_item) {
5972 - if (isset($content_item['text'])) {
5973 - $text_content .= $content_item['text'] . "\n";
5974 - }
5975 - }
5976 - //error_log("MXCHAT VECTORSTORE: Found text in content array for result[$index], length: " . strlen($text_content));
5977 - } else {
5978 - //error_log("MXCHAT VECTORSTORE: No text found for result[$index]. Keys: " . implode(', ', array_keys($search_result)));
5979 - }
5980 -
5981 - if (!empty($text_content)) {
5982 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
5983 - $content .= trim($text_content) . "\n\n";
5984 -
5985 - if (!empty($filename)) {
5986 - $content .= "Source: " . $filename . "\n\n";
5987 - }
5988 -
5989 - // Extract URLs from content
5990 - preg_match_all(
5991 - '#\bhttps?://[^\s<>"\']+#i',
5992 - $text_content,
5993 - $content_urls
5994 - );
5995 - if (!empty($content_urls[0])) {
5996 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5997 - }
5998 -
5999 - $matches_used++;
6000 - }
6001 -
6002 - // Store for similarity analysis
6003 - $all_matches[] = [
6004 - 'document_id' => $filename ?: ('result_' . $index),
6005 - 'similarity' => $score,
6006 - 'similarity_percentage' => round($score * 100, 2),
6007 - 'above_threshold' => true,
6008 - 'source_display' => $filename,
6009 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6010 - 'used_for_context' => true,
6011 - 'role_restriction' => 'public',
6012 - 'has_access' => true,
6013 - 'filtered_out' => false
6014 - ];
6015 - }
6016 - }
6017 -
6018 - // Also check for message content with annotations (citations)
6019 - if (isset($output_item['type']) && $output_item['type'] === 'message') {
6020 - if (isset($output_item['content']) && is_array($output_item['content'])) {
6021 - foreach ($output_item['content'] as $content_block) {
6022 - if (isset($content_block['annotations']) && is_array($content_block['annotations'])) {
6023 - foreach ($content_block['annotations'] as $annotation) {
6024 - if (isset($annotation['filename'])) {
6025 - $filename = $annotation['filename'];
6026 - $score = $annotation['score'] ?? 0;
6027 - $text_content = '';
6028 -
6029 - if (isset($annotation['content']) && is_array($annotation['content'])) {
6030 - foreach ($annotation['content'] as $ann_content) {
6031 - if (isset($ann_content['text'])) {
6032 - $text_content .= $ann_content['text'] . "\n";
6033 - }
6034 - }
6035 - }
6036 -
6037 - if (!empty($text_content) && $matches_used < $max_results) {
6038 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
6039 - $content .= trim($text_content) . "\n\n";
6040 - $content .= "Source: " . $filename . "\n\n";
6041 -
6042 - preg_match_all(
6043 - '#\bhttps?://[^\s<>"\']+#i',
6044 - $text_content,
6045 - $content_urls
6046 - );
6047 - if (!empty($content_urls[0])) {
6048 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6049 - }
6050 -
6051 - $matches_used++;
6052 -
6053 - $all_matches[] = [
6054 - 'document_id' => $filename,
6055 - 'similarity' => $score,
6056 - 'similarity_percentage' => round($score * 100, 2),
6057 - 'above_threshold' => true,
6058 - 'source_display' => $filename,
6059 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6060 - 'used_for_context' => true,
6061 - 'role_restriction' => 'public',
6062 - 'has_access' => true,
6063 - 'filtered_out' => false
6064 - ];
6065 - }
6066 - }
6067 - }
6068 - }
6069 - }
6070 - }
6071 - }
6072 - }
6073 - }
6074 -
6075 - // Store for testing panel
6076 - $this->last_similarity_analysis['top_matches'] = $all_matches;
6077 - $this->last_similarity_analysis['total_checked'] = count($all_matches);
6078 -
6079 - // Store unique valid URLs for validation
6080 - $this->current_valid_urls = array_unique($valid_urls);
6081 -
6082 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
6083 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
6084 -
6085 - //error_log("MXCHAT VECTORSTORE: ========== SEARCH COMPLETE ==========");
6086 - //error_log("MXCHAT VECTORSTORE: Matches used: " . $matches_used);
6087 - //error_log("MXCHAT VECTORSTORE: All matches count: " . count($all_matches));
6088 - //error_log("MXCHAT VECTORSTORE: Content length: " . strlen($content));
6089 - if ($matches_used > 0) {
6090 - //error_log("MXCHAT VECTORSTORE: Content preview: " . substr($content, 0, 500));
6091 - }
6092 -
6093 - // Check if citation links are enabled
6094 - $citation_links_enabled = ($mxchat_options['citation_links_toggle'] ?? 'on') === 'on';
6095 -
6096 - // Add response guidelines
6097 - if ($matches_used === 0) {
6098 - //error_log("MXCHAT VECTORSTORE: No matches found - returning empty reference message");
6099 - $content = "No reference information was found for this query.\n\n";
6100 - } else {
6101 - // Build response guidelines based on citation links setting
6102 - $content .= "\n## Response Guidelines ##\n" .
6103 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
6104 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
6105 - "If you don't have specific information or are uncertain about any details, it's always " .
6106 - "better to honestly say you don't know rather than making up or guessing at answers. " .
6107 - "When information is incomplete, let them know you are unsure.\n\n";
6108 -
6109 - // Only add hyperlink instructions if citation links are enabled
6110 - if ($citation_links_enabled) {
6111 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
6112 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
6113 - } else {
6114 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
6115 - "Simply provide helpful answers based on the reference information without citing sources.";
6116 - }
6117 - }
6118 -
6119 - //error_log("MXCHAT DEBUG: Vector Store search complete. Matches used: " . $matches_used);
6120 -
6121 - return trim($content);
6122 -}
6123 -
6124 -/**
6125 - * Check if the given model is an OpenAI chat model
6126 - *
6127 - * @param string $model The model ID
6128 - * @return bool True if it's an OpenAI model
6129 - */
6130 -private function is_openai_chat_model($model) {
6131 - $openai_prefixes = array('gpt-', 'o1-', 'o3-');
6132 - foreach ($openai_prefixes as $prefix) {
6133 - if (strpos($model, $prefix) === 0) {
6134 - return true;
6135 - }
6136 - }
6137 - return false;
6138 -}
6139 -
6140 -/**
6141 - * Get bot-specific Vector Store configuration
6142 - *
6143 - * @param string $bot_id The bot ID
6144 - * @return array Configuration array
6145 - */
6146 -private function get_bot_vectorstore_config($bot_id = 'default') {
6147 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
6148 -
6149 - // Default global settings
6150 - $default_config = array(
6151 - 'use_vectorstore' => ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1',
6152 - 'vectorstore_ids' => $vectorstore_options['mxchat_vectorstore_ids'] ?? '',
6153 - 'max_results' => $vectorstore_options['mxchat_vectorstore_max_results'] ?? 5
6154 - );
6155 -
6156 - // Allow multi-bot plugin to override with bot-specific settings
6157 - $bot_config = apply_filters('mxchat_get_bot_vectorstore_config', $default_config, $bot_id);
6158 -
6159 - // Preserve max_results from global settings if not set in bot config
6160 - if (!isset($bot_config['max_results'])) {
6161 - $bot_config['max_results'] = $default_config['max_results'];
6162 - }
6163 -
6164 - return $bot_config;
6165 -}
6166 -
6167 4555 private function mxchat_find_relevant_products($user_embedding) {
6168 4556 //error_log('MXChat Vector Search: Starting product search...');
6169 4557
6170 4558 // Retrieve the add-on settings from the database
@@ -6185,75 +4573,73 @@
6185 4573 }
6186 4574 private function find_relevant_products_wordpress($user_embedding) {
6187 4575 global $wpdb;
6188 4576 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
4577 + $cache_key = 'mxchat_system_prompt_embeddings';
4578 + $batch_size = 500;
6189 4579
6190 - if (!is_array($user_embedding)) {
6191 - return '';
6192 - }
4580 + // Original WordPress database search logic
4581 + // [Previous implementation remains the same]
4582 + $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
4583 + if ($embeddings === false) {
4584 + $embeddings = [];
4585 + $offset = 0;
6193 4586
6194 - // Streaming top-K pass: scan rows in small batches, keep only the top 3
6195 - // results above the similarity threshold. Peak memory is bounded by
6196 - // $batch_size embedding rows plus a 3-element top list.
6197 - $batch_size = 250;
6198 - $similarity_threshold = 0.85;
6199 - $top_k = 3;
6200 - $top_results = [];
6201 - $offset = 0;
4587 + do {
4588 + $query = $wpdb->prepare(
4589 + "SELECT id, embedding_vector
4590 + FROM {$system_prompt_table}
4591 + LIMIT %d OFFSET %d",
4592 + $batch_size,
4593 + $offset
4594 + );
6202 4595
6203 - do {
6204 - $batch = $wpdb->get_results($wpdb->prepare(
6205 - "SELECT id, embedding_vector
6206 - FROM {$system_prompt_table}
6207 - LIMIT %d OFFSET %d",
6208 - $batch_size,
6209 - $offset
6210 - ));
4596 + $batch = $wpdb->get_results($query);
4597 + if (empty($batch)) {
4598 + break;
4599 + }
6211 4600
6212 - if (empty($batch)) {
6213 - break;
6214 - }
4601 + $embeddings = array_merge($embeddings, $batch);
4602 + $offset += $batch_size;
6215 4603
6216 - foreach ($batch as $row) {
6217 - $database_embedding = $row->embedding_vector
6218 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
6219 - : null;
4604 + unset($batch);
6220 4605
6221 - if (!is_array($database_embedding)) {
6222 - unset($database_embedding);
6223 - continue;
6224 - }
4606 + } while (true);
6225 4607
4608 + if (empty($embeddings)) {
4609 + return '';
4610 + }
4611 + wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
4612 + }
4613 +
4614 + $relevant_results = [];
4615 + foreach ($embeddings as $embedding) {
4616 + $database_embedding = $embedding->embedding_vector
4617 + ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
4618 + : null;
4619 + if (is_array($database_embedding) && is_array($user_embedding)) {
6226 4620 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
6227 - unset($database_embedding);
6228 -
6229 - if ($similarity < $similarity_threshold) {
6230 - continue;
6231 - }
6232 -
6233 - // Insert into bounded top-K (kept sorted descending)
6234 - if (count($top_results) < $top_k) {
6235 - $top_results[] = ['id' => $row->id, 'similarity' => $similarity];
6236 - usort($top_results, function ($a, $b) {
6237 - return $b['similarity'] <=> $a['similarity'];
6238 - });
6239 - } elseif ($similarity > $top_results[$top_k - 1]['similarity']) {
6240 - $top_results[$top_k - 1] = ['id' => $row->id, 'similarity' => $similarity];
6241 - usort($top_results, function ($a, $b) {
6242 - return $b['similarity'] <=> $a['similarity'];
6243 - });
6244 - }
4621 + $relevant_results[] = [
4622 + 'id' => $embedding->id,
4623 + 'similarity' => $similarity
4624 + ];
6245 4625 }
4626 + unset($database_embedding);
4627 + }
6246 4628
6247 - unset($batch);
6248 - $offset += $batch_size;
6249 - } while (true);
4629 + // Use fixed threshold for products
4630 + $similarity_threshold = 0.85;
6250 4631
6251 - if (empty($top_results)) {
6252 - return '';
6253 - }
4632 + $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
4633 + return $result['similarity'] >= $similarity_threshold;
4634 + });
4635 + usort($relevant_results, function ($a, $b) {
4636 + return $b['similarity'] <=> $a['similarity'];
4637 + });
6254 4638
4639 + $top_results = array_slice($relevant_results, 0, 3);
6255 4640 $content = '';
4641 +
6256 4642 foreach ($top_results as $result) {
6257 4643 $chunk_content = $this->fetch_content_with_product_links($result['id']);
6258 4644 $content .= $chunk_content . "\n\n";
6259 4645 }
@@ -6362,61 +4748,23 @@
6362 4748
6363 4749 /**
6364 4750 * Get system instructions for a specific bot or default
6365 4751 * Checks for multi-bot add-on and uses bot-specific instructions if available
6366 - * Automatically strips URLs if citation links are disabled
6367 - * Replaces {visitor_name} placeholder with actual visitor name if available
6368 - *
6369 - * @param string $bot_id The bot ID to get instructions for
6370 - * @param string $session_id Optional session ID to lookup visitor name
6371 4752 */
6372 -private function get_system_instructions($bot_id = 'default', $session_id = '') {
6373 - $instructions = '';
6374 -
4753 +private function get_system_instructions($bot_id = 'default') {
6375 4754 // Check if multi-bot add-on is active
6376 4755 if (class_exists('MxChat_Multi_Bot_Core_Manager') && $bot_id !== 'default') {
6377 4756 // Get bot-specific options from multi-bot add-on
6378 4757 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
6379 -
4758 +
6380 4759 // If bot has custom system instructions, use those
6381 4760 if (!empty($bot_options['system_prompt_instructions'])) {
6382 - $instructions = $bot_options['system_prompt_instructions'];
4761 + return $bot_options['system_prompt_instructions'];
6383 4762 }
6384 4763 }
6385 -
4764 +
6386 4765 // Fall back to default system instructions
6387 - if (empty($instructions)) {
6388 - $instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6389 - }
6390 -
6391 - // Check if citation links are disabled - if so, strip URLs from instructions
6392 - $fresh_options = get_option('mxchat_options', []);
6393 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
6394 -
6395 - if (!$citation_links_enabled && !empty($instructions)) {
6396 - $instructions = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $instructions);
6397 - $instructions = preg_replace('/\s+/', ' ', trim($instructions)); // Clean up extra spaces
6398 - }
6399 -
6400 - // Replace {visitor_name} placeholder with actual visitor name if available
6401 - if (!empty($instructions) && !empty($session_id) && stripos($instructions, '{visitor_name}') !== false) {
6402 - $name_option_key = "mxchat_name_{$session_id}";
6403 - $visitor_name = get_option($name_option_key, '');
6404 -
6405 - if (!empty($visitor_name)) {
6406 - $instructions = str_ireplace('{visitor_name}', sanitize_text_field($visitor_name), $instructions);
6407 - } else {
6408 - // Remove placeholder if no name is available
6409 - $instructions = str_ireplace('{visitor_name}', '', $instructions);
6410 - $instructions = preg_replace('/\s{2,}/', ' ', trim($instructions)); // Clean up extra spaces
6411 - }
6412 - }
6413 -
6414 - // Allow developers to filter system instructions and process shortcodes
6415 - $instructions = apply_filters('mxchat_system_instructions', $instructions, $bot_id, $session_id);
6416 - $instructions = do_shortcode($instructions);
6417 -
6418 - return $instructions;
4766 + return isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6419 4767 }
6420 4768 /**
6421 4769 * Get the current bot ID from session or request context
6422 4770 */
@@ -6436,9 +4784,9 @@
6436 4784
6437 4785 // Fall back to default
6438 4786 return 'default';
6439 4787 }
6440 -private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $gemini_api_key, $openrouter_api_key, $conversation_history, $streaming = false, $session_id = '', $testing_data = null, $selected_model = 'gpt-5.1-chat-latest') {
4788 +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-4o') {
6441 4789 try {
6442 4790 if (!$relevant_content) {
6443 4791 $error_response = [
6444 4792 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
@@ -6637,27 +4985,9 @@
6637 4985 $error_response['testing_data'] = $testing_data;
6638 4986 }
6639 4987 return $error_response;
6640 4988 }
6641 -
6642 - // Check if web search is enabled for this OpenAI model
6643 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6644 - // Models that don't support web search
6645 - $unsupported_web_search_models = array('gpt-4.1-nano');
6646 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6647 -
6648 - if ($web_search_enabled && $model_supports_web_search) {
6649 - // Use Responses API (required for some models, or when web search is enabled)
6650 - return $this->mxchat_generate_response_openai_web_search(
6651 - $selected_model,
6652 - $api_key,
6653 - $conversation_history,
6654 - $relevant_content,
6655 - $session_id,
6656 - $testing_data,
6657 - $streaming
6658 - );
6659 - } elseif ($streaming) {
4989 + if ($streaming) {
6660 4990 return $this->mxchat_generate_response_openai_stream(
6661 4991 $selected_model,
6662 4992 $api_key,
6663 4993 $conversation_history,
@@ -6685,25 +5015,9 @@
6685 5015 $error_response['testing_data'] = $testing_data;
6686 5016 }
6687 5017 return $error_response;
6688 5018 }
6689 -
6690 - // Check if web search is enabled (default case also handles OpenAI models)
6691 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6692 - $unsupported_web_search_models = array('gpt-4.1-nano');
6693 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6694 -
6695 - if ($web_search_enabled && $model_supports_web_search) {
6696 - return $this->mxchat_generate_response_openai_web_search(
6697 - $selected_model,
6698 - $api_key,
6699 - $conversation_history,
6700 - $relevant_content,
6701 - $session_id,
6702 - $testing_data,
6703 - $streaming
6704 - );
6705 - } elseif ($streaming) {
5019 + if ($streaming) {
6706 5020 return $this->mxchat_generate_response_openai_stream(
6707 5021 $selected_model,
6708 5022 $api_key,
6709 5023 $conversation_history,
@@ -6747,9 +5061,9 @@
6747 5061 }
6748 5062 private function mxchat_generate_response_openrouter_stream($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
6749 5063 try {
6750 5064 $bot_id = $this->get_current_bot_id($session_id);
6751 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5065 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
6752 5066
6753 5067 if (!is_array($conversation_history)) {
6754 5068 $conversation_history = array();
6755 5069 }
@@ -6811,11 +5125,8 @@
6811 5125 'temperature' => 1,
6812 5126 'stream' => true
6813 5127 ]);
6814 5128
6815 - // Setup streaming headers now that we know we're actually streaming
6816 - $this->setup_streaming_headers();
6817 -
6818 5129 $ch = curl_init();
6819 5130 curl_setopt($ch, CURLOPT_URL, 'https://openrouter.ai/api/v1/chat/completions');
6820 5131 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
6821 5132 curl_setopt($ch, CURLOPT_POST, true);
@@ -6904,33 +5215,13 @@
6904 5215
6905 5216 curl_close($ch);
6906 5217
6907 5218 if (!empty($full_response) && !empty($session_id)) {
6908 - // Prepare RAG context for streaming response
6909 - $rag_context_for_storage = null;
6910 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
6911 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
6912 -
6913 - if ($has_rag_data || $has_action_data) {
6914 - $rag_context_for_storage = [];
6915 -
6916 - if ($has_rag_data) {
6917 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
6918 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
6919 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
6920 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
6921 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
6922 - }
6923 -
6924 - if ($has_action_data) {
6925 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
6926 - }
6927 - }
6928 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
5219 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
6929 5220 }
6930 -
5221 +
6931 5222 return true;
6932 -
5223 +
6933 5224 } catch (Exception $e) {
6934 5225 $regular_response = $this->mxchat_generate_response_openrouter(
6935 5226 $selected_model,
6936 5227 $openrouter_api_key,
@@ -6957,9 +5248,9 @@
6957 5248 try {
6958 5249 $bot_id = $this->get_current_bot_id($session_id);
6959 5250
6960 5251 // Get system prompt instructions using centralized function
6961 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5252 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
6962 5253
6963 5254 // Ensure conversation_history is an array
6964 5255 if (!is_array($conversation_history)) {
6965 5256 $conversation_history = array();
@@ -7021,9 +5312,8 @@
7021 5312
7022 5313 // Check if this is a GPT-5 model (supports reasoning_effort parameter)
7023 5314 $is_gpt5_model = (
7024 5315 strpos($selected_model, 'gpt-5') === 0 ||
7025 - $selected_model === 'gpt-5.2' ||
7026 5316 $selected_model === 'gpt-5.1-2025-11-13' ||
7027 5317 $selected_model === 'gpt-5' ||
7028 5318 $selected_model === 'gpt-5-mini' ||
7029 5319 $selected_model === 'gpt-5-nano'
@@ -7036,27 +5326,20 @@
7036 5326 'temperature' => 1,
7037 5327 'stream' => true
7038 5328 ];
7039 5329
7040 - // Add reasoning_effort only for GPT-5 models that support it
7041 - // These chat models don't support reasoning_effort parameter
7042 - $no_reasoning_models = array('gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
7043 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
5330 + // Add reasoning_effort only for GPT-5 models
5331 + if ($is_gpt5_model) {
7044 5332 // GPT-5.1 uses 'low' instead of 'minimal'
7045 5333 if ($selected_model === 'gpt-5.1-2025-11-13') {
7046 5334 $request_body['reasoning_effort'] = 'low';
7047 - } elseif ($selected_model === 'gpt-5.4') {
7048 - $request_body['reasoning_effort'] = 'none';
7049 5335 } else {
7050 - $request_body['reasoning_effort'] = 'minimal';
5336 + $request_body['reasoning_effort'] = 'minimal'; // For other GPT-5 models
7051 5337 }
7052 5338 }
7053 5339
7054 5340 $body = json_encode($request_body);
7055 5341
7056 - // Setup streaming headers now that we know we're actually streaming
7057 - $this->setup_streaming_headers();
7058 -
7059 5342 // Use cURL for streaming support
7060 5343 $ch = curl_init();
7061 5344 curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
7062 5345 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -7171,36 +5454,16 @@
7171 5454 return true;
7172 5455 }
7173 5456
7174 5457 curl_close($ch);
7175 -
5458 +
7176 5459 // Save the complete response to maintain chat persistence
7177 5460 if (!empty($full_response) && !empty($session_id)) {
7178 - // Prepare RAG context for streaming response
7179 - $rag_context_for_storage = null;
7180 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7181 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7182 -
7183 - if ($has_rag_data || $has_action_data) {
7184 - $rag_context_for_storage = [];
7185 -
7186 - if ($has_rag_data) {
7187 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7188 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7189 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7190 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7191 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7192 - }
7193 -
7194 - if ($has_action_data) {
7195 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7196 - }
7197 - }
7198 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
5461 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
7199 5462 }
7200 -
5463 +
7201 5464 return true; // Indicate streaming completed successfully
7202 -
5465 +
7203 5466 } catch (Exception $e) {
7204 5467 // Fallback to regular response
7205 5468 $regular_response = $this->mxchat_generate_response_openai(
7206 5469 $selected_model,
@@ -7238,363 +5501,8 @@
7238 5501 echo json_encode($response_data);
7239 5502 return true;
7240 5503 }
7241 5504 }
7242 -
7243 -/**
7244 - * Generate response using OpenAI Responses API with web search tool
7245 - * This uses the newer Responses API which supports web search functionality
7246 - */
7247 -private function mxchat_generate_response_openai_web_search($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null, $streaming = false) {
7248 - try {
7249 - $bot_id = $this->get_current_bot_id($session_id);
7250 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7251 -
7252 - if (!is_array($conversation_history)) {
7253 - $conversation_history = array();
7254 - }
7255 -
7256 - // Build the input for Responses API
7257 - // The Responses API uses a different format - we need to construct the input properly
7258 - $input_parts = [];
7259 -
7260 - // Add system instructions as context
7261 - $system_context = $system_prompt_instructions . "\n\n" . $relevant_content;
7262 -
7263 - // Build conversation as input items for Responses API
7264 - foreach ($conversation_history as $message) {
7265 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7266 - $role = $message['role'];
7267 - if ($role === 'bot' || $role === 'agent') {
7268 - $role = 'assistant';
7269 - }
7270 - if (!in_array($role, ['assistant', 'user'])) {
7271 - $role = 'user';
7272 - }
7273 - $input_parts[] = [
7274 - 'type' => 'message',
7275 - 'role' => $role,
7276 - 'content' => $message['content']
7277 - ];
7278 - }
7279 - }
7280 -
7281 - // Build request body for Responses API
7282 - $request_body = [
7283 - 'model' => $selected_model,
7284 - 'input' => $input_parts,
7285 - 'instructions' => $system_context,
7286 - 'stream' => $streaming
7287 - ];
7288 -
7289 - // Only add web search tool if web search is enabled in settings
7290 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
7291 - if ($web_search_enabled) {
7292 - $request_body['tools'] = [
7293 - ['type' => 'web_search']
7294 - ];
7295 - }
7296 -
7297 - // Add reasoning effort for supported models
7298 - $is_gpt5_model = strpos($selected_model, 'gpt-5') === 0;
7299 - $no_reasoning_web = array('gpt-5.2', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
7300 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_web, true)) {
7301 - if ($selected_model === 'gpt-5.1-2025-11-13') {
7302 - $request_body['reasoning'] = ['effort' => 'low'];
7303 - } elseif ($selected_model === 'gpt-5.4') {
7304 - $request_body['reasoning'] = ['effort' => 'low'];
7305 - }
7306 - }
7307 -
7308 - //error_log("MXCHAT WEB SEARCH: Request body: " . json_encode($request_body));
7309 -
7310 - if ($streaming) {
7311 - return $this->mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data);
7312 - } else {
7313 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7314 - }
7315 -
7316 - } catch (Exception $e) {
7317 - //error_log("MXCHAT WEB SEARCH ERROR: " . $e->getMessage());
7318 - return [
7319 - 'error' => sprintf(esc_html__('Web search error: %s', 'mxchat'), esc_html($e->getMessage())),
7320 - 'error_code' => 'web_search_exception'
7321 - ];
7322 - }
7323 -}
7324 -
7325 -/**
7326 - * Handle non-streaming web search response
7327 - */
7328 -private function mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7329 - $request_body['stream'] = false;
7330 -
7331 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
7332 - 'headers' => array(
7333 - 'Authorization' => 'Bearer ' . $api_key,
7334 - 'Content-Type' => 'application/json'
7335 - ),
7336 - 'body' => json_encode($request_body),
7337 - 'timeout' => 90
7338 - ));
7339 -
7340 - if (is_wp_error($response)) {
7341 - //error_log("MXCHAT WEB SEARCH ERROR: WP Error: " . $response->get_error_message());
7342 - return [
7343 - 'error' => esc_html__('Failed to connect to OpenAI web search API', 'mxchat'),
7344 - 'error_code' => 'web_search_connection_error'
7345 - ];
7346 - }
7347 -
7348 - $response_code = wp_remote_retrieve_response_code($response);
7349 - $response_body = wp_remote_retrieve_body($response);
7350 -
7351 - //error_log("MXCHAT WEB SEARCH: Response code: " . $response_code);
7352 - //error_log("MXCHAT WEB SEARCH: Response body (first 2000): " . substr($response_body, 0, 2000));
7353 -
7354 - if ($response_code !== 200) {
7355 - $error_data = json_decode($response_body, true);
7356 - $error_message = $error_data['error']['message'] ?? 'Unknown API error';
7357 - return [
7358 - 'error' => sprintf(esc_html__('OpenAI API error: %s', 'mxchat'), esc_html($error_message)),
7359 - 'error_code' => 'web_search_api_error'
7360 - ];
7361 - }
7362 -
7363 - $result = json_decode($response_body, true);
7364 -
7365 - if (json_last_error() !== JSON_ERROR_NONE) {
7366 - return [
7367 - 'error' => esc_html__('Invalid response from OpenAI', 'mxchat'),
7368 - 'error_code' => 'web_search_json_error'
7369 - ];
7370 - }
7371 -
7372 - // Extract the response text and citations from Responses API format
7373 - $output_text = '';
7374 - $citations = [];
7375 -
7376 - if (isset($result['output'])) {
7377 - foreach ($result['output'] as $output_item) {
7378 - if ($output_item['type'] === 'message' && isset($output_item['content'])) {
7379 - foreach ($output_item['content'] as $content_item) {
7380 - if ($content_item['type'] === 'output_text') {
7381 - $output_text .= $content_item['text'];
7382 -
7383 - // Extract citations/annotations
7384 - if (isset($content_item['annotations'])) {
7385 - foreach ($content_item['annotations'] as $annotation) {
7386 - if ($annotation['type'] === 'url_citation') {
7387 - $citations[] = [
7388 - 'url' => $annotation['url'],
7389 - 'title' => $annotation['title'] ?? ''
7390 - ];
7391 - }
7392 - }
7393 - }
7394 - }
7395 - }
7396 - }
7397 - }
7398 - }
7399 -
7400 - // If we have citations, append them to the response
7401 - if (!empty($citations)) {
7402 - $output_text .= "\n\n**Sources:**\n";
7403 - $seen_urls = [];
7404 - foreach ($citations as $citation) {
7405 - if (!in_array($citation['url'], $seen_urls)) {
7406 - $seen_urls[] = $citation['url'];
7407 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7408 - $output_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7409 - }
7410 - }
7411 - }
7412 -
7413 - // Transcript save is handled by the main handler (mxchat_handle_chat_request)
7414 - // which includes rag_context for the "sources" link in transcripts.
7415 -
7416 - return $output_text;
7417 -}
7418 -
7419 -/**
7420 - * Handle streaming web search response using Responses API
7421 - */
7422 -private function mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7423 - $request_body['stream'] = true;
7424 -
7425 - // Check if we can stream
7426 - if (headers_sent() || !function_exists('curl_init')) {
7427 - // Fallback to non-streaming
7428 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7429 - }
7430 -
7431 - // Setup streaming headers
7432 - $this->setup_streaming_headers();
7433 -
7434 - $ch = curl_init();
7435 - curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/responses');
7436 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7437 - curl_setopt($ch, CURLOPT_POST, true);
7438 - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request_body));
7439 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7440 - 'Content-Type: application/json',
7441 - 'Authorization: Bearer ' . $api_key
7442 - ));
7443 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7444 - curl_setopt($ch, CURLOPT_TIMEOUT, 120);
7445 -
7446 - $full_response = '';
7447 - $stream_started = false;
7448 - $buffer = '';
7449 - $citations = [];
7450 -
7451 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$citations, $testing_data) {
7452 - // Send testing data as first event if available
7453 - if (!$stream_started && $testing_data !== null) {
7454 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7455 - flush();
7456 - $stream_started = true;
7457 - }
7458 -
7459 - $buffer .= $data;
7460 - $lines = explode("\n", $buffer);
7461 - $buffer = array_pop($lines);
7462 -
7463 - foreach ($lines as $line) {
7464 - if (trim($line) === '') continue;
7465 - if (strpos($line, 'data: ') !== 0) continue;
7466 -
7467 - $json_str = substr($line, 6);
7468 -
7469 - if (trim($json_str) === '[DONE]') {
7470 - // Append citations if we have any
7471 - if (!empty($citations)) {
7472 - $citation_text = "\n\n**Sources:**\n";
7473 - $seen_urls = [];
7474 - foreach ($citations as $citation) {
7475 - if (!in_array($citation['url'], $seen_urls)) {
7476 - $seen_urls[] = $citation['url'];
7477 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7478 - $citation_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7479 - }
7480 - }
7481 - echo "data: " . json_encode(['content' => $citation_text]) . "\n\n";
7482 - $full_response .= $citation_text;
7483 - flush();
7484 - }
7485 - echo "data: [DONE]\n\n";
7486 - flush();
7487 - continue;
7488 - }
7489 -
7490 - $json = json_decode(trim($json_str), true);
7491 - if (!$json) continue;
7492 -
7493 - // Handle Responses API streaming events
7494 - // The format is different from Chat Completions
7495 - if (isset($json['type'])) {
7496 - switch ($json['type']) {
7497 - case 'response.output_text.delta':
7498 - // Text content delta
7499 - if (isset($json['delta'])) {
7500 - $content = $json['delta'];
7501 - $full_response .= $content;
7502 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
7503 - flush();
7504 - }
7505 - break;
7506 -
7507 - case 'response.output_item.done':
7508 - // Check for citations in completed items
7509 - if (isset($json['item']['content'])) {
7510 - foreach ($json['item']['content'] as $content_item) {
7511 - if (isset($content_item['annotations'])) {
7512 - foreach ($content_item['annotations'] as $annotation) {
7513 - if ($annotation['type'] === 'url_citation') {
7514 - $citations[] = [
7515 - 'url' => $annotation['url'],
7516 - 'title' => $annotation['title'] ?? ''
7517 - ];
7518 - }
7519 - }
7520 - }
7521 - }
7522 - }
7523 - break;
7524 - }
7525 - }
7526 - }
7527 -
7528 - return strlen($data);
7529 - });
7530 -
7531 - $response = curl_exec($ch);
7532 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7533 -
7534 - if (curl_errno($ch) || $http_code !== 200) {
7535 - $curl_error = curl_error($ch);
7536 - curl_close($ch);
7537 -
7538 - //error_log("MXCHAT WEB SEARCH STREAM ERROR: HTTP $http_code, cURL error: $curl_error");
7539 -
7540 - // Fallback to non-streaming
7541 - $fallback_response = $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7542 -
7543 - if (is_array($fallback_response) && isset($fallback_response['error'])) {
7544 - echo "data: " . json_encode([
7545 - 'error' => true,
7546 - 'error_message' => $fallback_response['error'],
7547 - 'error_code' => $fallback_response['error_code'] ?? 'web_search_error'
7548 - ]) . "\n\n";
7549 - echo "data: [DONE]\n\n";
7550 - flush();
7551 - return true;
7552 - }
7553 -
7554 - $response_data = [
7555 - 'text' => $fallback_response,
7556 - 'html' => '',
7557 - 'session_id' => $session_id
7558 - ];
7559 - if ($testing_data !== null) {
7560 - $response_data['testing_data'] = $testing_data;
7561 - }
7562 - header('Content-Type: application/json');
7563 - echo json_encode($response_data);
7564 - return true;
7565 - }
7566 -
7567 - curl_close($ch);
7568 -
7569 - // Save the complete response with RAG context so the "sources" link
7570 - // appears in transcripts — mirrors the pattern used by Claude/OpenAI streaming.
7571 - if (!empty($full_response) && !empty($session_id)) {
7572 - $rag_context_for_storage = null;
7573 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7574 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7575 -
7576 - if ($has_rag_data || $has_action_data) {
7577 - $rag_context_for_storage = [];
7578 -
7579 - if ($has_rag_data) {
7580 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7581 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7582 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7583 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7584 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7585 - }
7586 -
7587 - if ($has_action_data) {
7588 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7589 - }
7590 - }
7591 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
7592 - }
7593 -
7594 - return true;
7595 -}
7596 -
7597 5505 private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7598 5506 try {
7599 5507 // Get bot ID from session or request
7600 5508 $bot_id = $this->get_current_bot_id($session_id);
@@ -7599,9 +5507,9 @@
7599 5507 // Get bot ID from session or request
7600 5508 $bot_id = $this->get_current_bot_id($session_id);
7601 5509
7602 5510 // Get system prompt instructions using centralized function
7603 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5511 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
7604 5512 // Ensure conversation_history is an array
7605 5513 if (!is_array($conversation_history)) {
7606 5514 $conversation_history = array();
7607 5515 }
@@ -7678,11 +5586,8 @@
7678 5586 echo json_encode($response_data);
7679 5587 return true; // Indicate we handled the response
7680 5588 }
7681 5589
7682 - // Setup streaming headers now that we know we're actually streaming
7683 - $this->setup_streaming_headers();
7684 -
7685 5590 // Use cURL for streaming support
7686 5591 $ch = curl_init();
7687 5592 curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
7688 5593 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -7821,29 +5726,9 @@
7821 5726 }
7822 5727
7823 5728 // Save the complete response to maintain chat persistence
7824 5729 if (!empty($full_response) && !empty($session_id)) {
7825 - // Prepare RAG context for streaming response
7826 - $rag_context_for_storage = null;
7827 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7828 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7829 -
7830 - if ($has_rag_data || $has_action_data) {
7831 - $rag_context_for_storage = [];
7832 -
7833 - if ($has_rag_data) {
7834 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7835 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7836 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7837 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7838 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7839 - }
7840 -
7841 - if ($has_action_data) {
7842 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7843 - }
7844 - }
7845 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
5730 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
7846 5731 }
7847 5732
7848 5733 return true; // Indicate streaming completed successfully
7849 5734
@@ -7894,9 +5779,9 @@
7894 5779 // Get bot ID from session or request
7895 5780 $bot_id = $this->get_current_bot_id($session_id);
7896 5781
7897 5782 // Get system prompt instructions using centralized function
7898 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5783 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
7899 5784
7900 5785 // Ensure conversation_history is an array
7901 5786 if (!is_array($conversation_history)) {
7902 5787 $conversation_history = array();
@@ -7965,11 +5850,8 @@
7965 5850 'temperature' => 0.8,
7966 5851 'stream' => true
7967 5852 ]);
7968 5853
7969 - // Setup streaming headers now that we know we're actually streaming
7970 - $this->setup_streaming_headers();
7971 -
7972 5854 // Use cURL for streaming support
7973 5855 $ch = curl_init();
7974 5856 curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
7975 5857 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -8070,39 +5952,19 @@
8070 5952 return true;
8071 5953 }
8072 5954
8073 5955 curl_close($ch);
8074 -
5956 +
8075 5957 // Save the complete response to maintain chat persistence
8076 5958 if (!empty($full_response) && !empty($session_id)) {
8077 - // Prepare RAG context for streaming response
8078 - $rag_context_for_storage = null;
8079 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8080 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8081 -
8082 - if ($has_rag_data || $has_action_data) {
8083 - $rag_context_for_storage = [];
8084 -
8085 - if ($has_rag_data) {
8086 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8087 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8088 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8089 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8090 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8091 - }
8092 -
8093 - if ($has_action_data) {
8094 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8095 - }
8096 - }
8097 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
5959 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
8098 5960 }
8099 -
5961 +
8100 5962 return true; // Indicate streaming completed successfully
8101 -
5963 +
8102 5964 } catch (Exception $e) {
8103 5965 //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
8104 -
5966 +
8105 5967 // Fallback to regular response
8106 5968 $regular_response = $this->mxchat_generate_response_xai(
8107 5969 $selected_model,
8108 5970 $xai_api_key,
@@ -8131,9 +5993,9 @@
8131 5993 // Get bot ID from session or request
8132 5994 $bot_id = $this->get_current_bot_id($session_id);
8133 5995
8134 5996 // Get system prompt instructions using centralized function
8135 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
5997 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8136 5998
8137 5999 // Ensure conversation_history is an array
8138 6000 if (!is_array($conversation_history)) {
8139 6001 $conversation_history = array();
@@ -8202,11 +6064,8 @@
8202 6064 'temperature' => 0.8,
8203 6065 'stream' => true
8204 6066 ]);
8205 6067
8206 - // Setup streaming headers now that we know we're actually streaming
8207 - $this->setup_streaming_headers();
8208 -
8209 6068 // Use cURL for streaming support
8210 6069 $ch = curl_init();
8211 6070 curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
8212 6071 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -8321,39 +6180,19 @@
8321 6180 return true;
8322 6181 }
8323 6182
8324 6183 curl_close($ch);
8325 -
6184 +
8326 6185 // Save the complete response to maintain chat persistence
8327 6186 if (!empty($full_response) && !empty($session_id)) {
8328 - // Prepare RAG context for streaming response
8329 - $rag_context_for_storage = null;
8330 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8331 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8332 -
8333 - if ($has_rag_data || $has_action_data) {
8334 - $rag_context_for_storage = [];
8335 -
8336 - if ($has_rag_data) {
8337 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8338 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8339 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8340 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8341 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8342 - }
8343 -
8344 - if ($has_action_data) {
8345 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8346 - }
8347 - }
8348 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
6187 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
8349 6188 }
8350 -
6189 +
8351 6190 return true; // Indicate streaming completed successfully
8352 -
6191 +
8353 6192 } catch (Exception $e) {
8354 6193 //error_log("MxChat DeepSeek streaming exception: " . $e->getMessage());
8355 -
6194 +
8356 6195 // Fallback to regular response
8357 6196 $regular_response = $this->mxchat_generate_response_deepseek(
8358 6197 $selected_model,
8359 6198 $deepseek_api_key,
@@ -8395,9 +6234,9 @@
8395 6234 $conversation_history = array();
8396 6235 }
8397 6236
8398 6237 $bot_id = $this->get_current_bot_id('');
8399 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6238 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8400 6239
8401 6240 $formatted_conversation = array();
8402 6241
8403 6242 $formatted_conversation[] = array(
@@ -8497,9 +6336,9 @@
8497 6336 // Get bot ID from session or request
8498 6337 $bot_id = $this->get_current_bot_id($session_id);
8499 6338
8500 6339 // Get system prompt instructions using centralized function
8501 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6340 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8502 6341
8503 6342 // Clean and validate conversation history
8504 6343 foreach ($conversation_history as &$message) {
8505 6344 // Convert bot and agent roles to assistant
@@ -8606,9 +6445,9 @@
8606 6445 // Get bot ID from session or request
8607 6446 $bot_id = $this->get_current_bot_id('');
8608 6447
8609 6448 // Get system prompt instructions using centralized function
8610 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6449 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8611 6450
8612 6451 // Create a new array for the formatted conversation
8613 6452 $formatted_conversation = array();
8614 6453
@@ -8640,9 +6479,8 @@
8640 6479
8641 6480 // Check if this is a GPT-5 model (supports reasoning_effort parameter)
8642 6481 $is_gpt5_model = (
8643 6482 strpos($selected_model, 'gpt-5') === 0 ||
8644 - $selected_model === 'gpt-5.2' ||
8645 6483 $selected_model === 'gpt-5.1-2025-11-13' ||
8646 6484 $selected_model === 'gpt-5' ||
8647 6485 $selected_model === 'gpt-5-mini' ||
8648 6486 $selected_model === 'gpt-5-nano'
@@ -8655,19 +6493,15 @@
8655 6493 'temperature' => 1,
8656 6494 'stream' => false
8657 6495 ];
8658 6496
8659 - // Add reasoning_effort only for GPT-5 models that support it
8660 - // These chat models don't support reasoning_effort parameter
8661 - $no_reasoning_models = array('gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
8662 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
6497 + // Add reasoning_effort only for GPT-5 models
6498 + if ($is_gpt5_model) {
8663 6499 // GPT-5.1 uses 'low' instead of 'minimal'
8664 6500 if ($selected_model === 'gpt-5.1-2025-11-13') {
8665 6501 $request_body['reasoning_effort'] = 'low';
8666 - } elseif ($selected_model === 'gpt-5.4') {
8667 - $request_body['reasoning_effort'] = 'none';
8668 6502 } else {
8669 - $request_body['reasoning_effort'] = 'minimal';
6503 + $request_body['reasoning_effort'] = 'minimal'; // For other GPT-5 models
8670 6504 }
8671 6505 }
8672 6506
8673 6507 $body = json_encode($request_body);
@@ -8778,9 +6612,9 @@
8778 6612 // Get bot ID from session or request
8779 6613 $bot_id = $this->get_current_bot_id($session_id);
8780 6614
8781 6615 // Get system prompt instructions using centralized function
8782 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6616 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8783 6617
8784 6618 // Add system prompt to relevant content
8785 6619 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
8786 6620
@@ -8977,9 +6811,9 @@
8977 6811 // Get bot ID from session or request
8978 6812 $bot_id = $this->get_current_bot_id($session_id);
8979 6813
8980 6814 // Get system prompt instructions using centralized function
8981 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6815 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
8982 6816
8983 6817 // Create a new array for the formatted conversation
8984 6818 $formatted_conversation = array();
8985 6819
@@ -9136,9 +6970,9 @@
9136 6970 // Get bot ID from session or request
9137 6971 $bot_id = $this->get_current_bot_id($session_id);
9138 6972
9139 6973 // Get system prompt instructions using centralized function
9140 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6974 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
9141 6975
9142 6976 // Add system prompt to relevant content
9143 6977 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
9144 6978
@@ -9234,11 +7068,9 @@
9234 7068 ]
9235 7069 ]);
9236 7070
9237 7071 // Prepare the API endpoint
9238 - // Use v1beta for preview models (Gemini 3, experimental), v1 for stable models
9239 - $api_version = (strpos($selected_model, 'preview') !== false || strpos($selected_model, 'exp') !== false) ? 'v1beta' : 'v1';
9240 - $api_endpoint = 'https://generativelanguage.googleapis.com/' . $api_version . '/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
7072 + $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
9241 7073
9242 7074 // Set up the API request
9243 7075 $args = [
9244 7076 'body' => $body,
@@ -9280,9 +7112,9 @@
9280 7112
9281 7113
9282 7114 public function test_streaming_request() {
9283 7115 $options = get_option('mxchat_options', []);
9284 - $model = $options['model'] ?? 'gpt-5.1-chat-latest';
7116 + $model = $options['model'] ?? 'gpt-4o';
9285 7117
9286 7118 // Detect provider from model prefix
9287 7119 $provider = strtolower(explode('-', $model)[0]);
9288 7120
@@ -9465,13 +7297,17 @@
9465 7297 }
9466 7298
9467 7299
9468 7300 public function mxchat_enqueue_scripts_styles() {
9469 - // Fetch options from the database first to check loading strategy
9470 - $this->options = get_option('mxchat_options');
9471 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
9472 -
9473 - // Always enqueue CSS immediately
7301 + // Enqueue the script
7302 + wp_enqueue_script(
7303 + 'mxchat-chat-js',
7304 + plugin_dir_url(__FILE__) . '../js/chat-script.js',
7305 + array('jquery'),
7306 + MXCHAT_VERSION,
7307 + true
7308 + );
7309 + // Enqueue the CSS
9474 7310 wp_enqueue_style(
9475 7311 'mxchat-chat-css',
9476 7312 plugin_dir_url(__FILE__) . '../css/chat-style.css',
9477 7313 array(),
@@ -9476,43 +7312,17 @@
9476 7312 plugin_dir_url(__FILE__) . '../css/chat-style.css',
9477 7313 array(),
9478 7314 MXCHAT_VERSION
9479 7315 );
9480 -
9481 - // Handle script loading based on strategy
9482 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
9483 - // Enqueue the script normally
9484 - wp_enqueue_script(
9485 - 'mxchat-chat-js',
9486 - plugin_dir_url(__FILE__) . '../js/chat-script.js',
9487 - array('jquery'),
9488 - MXCHAT_VERSION,
9489 - true
9490 - );
9491 -
9492 - // Add defer attribute if strategy is 'defer'
9493 - if ($loading_strategy === 'defer') {
9494 - wp_script_add_data('mxchat-chat-js', 'strategy', 'defer');
9495 - }
9496 - } else {
9497 - // For delay or interaction-based loading, we'll use a custom loader
9498 - // Don't enqueue the main script - we'll load it dynamically
9499 - add_action('wp_footer', array($this, 'mxchat_output_delayed_script_loader'), 99);
9500 - }
9501 -
7316 + // Fetch options from the database
7317 + $this->options = get_option('mxchat_options');
9502 7318 $prompts_options = get_option('mxchat_prompts_options', array());
9503 -
9504 - // Check if AI theme is active - if so, skip inline colors in JavaScript
9505 - $theme_options = get_option('mxchat_theme_options', array());
9506 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9507 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9508 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9509 -
7319 +
9510 7320 // Prepare settings for JavaScript
9511 7321 $style_settings = array(
9512 7322 'ajax_url' => admin_url('admin-ajax.php'),
9513 7323 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9514 - 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
7324 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
9515 7325 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
9516 7326 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
9517 7327 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
9518 7328 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
@@ -9539,134 +7349,15 @@
9539 7349 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9540 7350 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off', // FIXED
9541 7351 'initial_email_state' => null, // Also fixed this undefined variable
9542 7352 'skip_email_check' => true,
9543 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9544 - 'skip_inline_colors' => $skip_inline_colors,
9545 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
7353 + 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
9546 7354 );
9547 -
9548 - // For normal/defer loading, use wp_localize_script
9549 - // For delayed loading, we store settings in a transient to be output inline
9550 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
9551 - wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
9552 - } else {
9553 - // Store settings for the delayed loader to use
9554 - set_transient('mxchat_delayed_settings_' . get_current_user_id(), $style_settings, 60);
9555 - }
7355 + // Pass the settings to the script
7356 + wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
9556 7357 }
9557 7358
9558 -/**
9559 - * Output the delayed script loader for performance optimization
9560 - */
9561 -public function mxchat_output_delayed_script_loader() {
9562 - $this->options = get_option('mxchat_options');
9563 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
9564 - $script_url = plugin_dir_url(__FILE__) . '../js/chat-script.js?ver=' . MXCHAT_VERSION;
9565 7359
9566 - // Get the stored settings
9567 - $prompts_options = get_option('mxchat_prompts_options', array());
9568 - $theme_options = get_option('mxchat_theme_options', array());
9569 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9570 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9571 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9572 -
9573 - $style_settings = array(
9574 - 'ajax_url' => admin_url('admin-ajax.php'),
9575 - 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9576 - 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
9577 - 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
9578 - 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
9579 - 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
9580 - 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
9581 - 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
9582 - 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
9583 - 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
9584 - 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
9585 - 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
9586 - 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
9587 - 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
9588 - 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
9589 - 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
9590 - 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
9591 - 'icon_color' => $this->options['icon_color'] ?? '#fff',
9592 - 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
9593 - 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
9594 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
9595 - 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
9596 - 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
9597 - 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
9598 - 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
9599 - 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
9600 - 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
9601 - 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9602 - 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off',
9603 - 'initial_email_state' => null,
9604 - 'skip_email_check' => true,
9605 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9606 - 'skip_inline_colors' => $skip_inline_colors,
9607 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
9608 - );
9609 -
9610 - // Determine delay time based on strategy
9611 - $delay_ms = 0;
9612 - switch ($loading_strategy) {
9613 - case 'delay_1s':
9614 - $delay_ms = 1000;
9615 - break;
9616 - case 'delay_3s':
9617 - $delay_ms = 3000;
9618 - break;
9619 - case 'delay_5s':
9620 - $delay_ms = 5000;
9621 - break;
9622 - }
9623 -
9624 - ?>
9625 - <script type="text/javascript">
9626 - (function() {
9627 - var mxchatLoaded = false;
9628 - var mxchatChat = <?php echo wp_json_encode($style_settings); ?>;
9629 - window.mxchatChat = mxchatChat;
9630 -
9631 - function loadMxChatScript() {
9632 - if (mxchatLoaded) return;
9633 - mxchatLoaded = true;
9634 -
9635 - function appendChatScript() {
9636 - var script = document.createElement('script');
9637 - script.src = <?php echo wp_json_encode($script_url); ?>;
9638 - script.type = 'text/javascript';
9639 - document.body.appendChild(script);
9640 - }
9641 -
9642 - if (typeof jQuery !== 'undefined') {
9643 - appendChatScript();
9644 - } else {
9645 - var jq = document.createElement('script');
9646 - jq.src = <?php echo wp_json_encode(includes_url('js/jquery/jquery.min.js')); ?>;
9647 - jq.onload = appendChatScript;
9648 - document.body.appendChild(jq);
9649 - }
9650 - }
9651 -
9652 - <?php if ($loading_strategy === 'on_interaction'): ?>
9653 - // Load on user interaction
9654 - var events = ['scroll', 'mousemove', 'touchstart', 'keydown', 'click'];
9655 - events.forEach(function(evt) {
9656 - window.addEventListener(evt, loadMxChatScript, {once: true, passive: true});
9657 - });
9658 - // Fallback: load after 8 seconds if no interaction
9659 - setTimeout(loadMxChatScript, 8000);
9660 - <?php else: ?>
9661 - // Load after specified delay
9662 - setTimeout(loadMxChatScript, <?php echo intval($delay_ms); ?>);
9663 - <?php endif; ?>
9664 - })();
9665 - </script>
9666 - <?php
9667 -}
9668 -
9669 7360 /**
9670 7361 * Setup the cron jobs for rate limits with guard against multiple calls
9671 7362 */
9672 7363 public function setup_rate_limit_cron_jobs() {
@@ -10223,9 +7914,9 @@
10223 7914 ? $this->options['system_prompt_instructions']
10224 7915 : 'No system prompt configured';
10225 7916
10226 7917 // Get selected model
10227 - $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
7918 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
10228 7919
10229 7920 // Check if OpenRouter is being used
10230 7921 $is_openrouter = ($selected_model === 'openrouter');
10231 7922 $openrouter_model = '';
@@ -10293,42 +7984,24 @@
10293 7984 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10294 7985 wp_send_json_error(['message' => 'Invalid nonce']);
10295 7986 return;
10296 7987 }
10297 -
7988 +
10298 7989 // Only allow admin users
10299 7990 if (!current_user_can('administrator')) {
10300 7991 wp_send_json_error(['message' => 'Unauthorized']);
10301 7992 return;
10302 7993 }
10303 -
10304 - // Check OpenAI Vector Store first (takes priority)
10305 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
10306 - $use_vectorstore = (isset($vectorstore_options['mxchat_use_openai_vectorstore']) && $vectorstore_options['mxchat_use_openai_vectorstore'] === '1');
10307 -
10308 - if ($use_vectorstore) {
10309 - $vectorstore_ids = $vectorstore_options['mxchat_openai_vectorstore_ids'] ?? '';
10310 - $id_count = !empty($vectorstore_ids) ? count(array_filter(array_map('trim', explode(',', $vectorstore_ids)))) : 0;
10311 -
10312 - $kb_info = [
10313 - 'type' => 'OpenAI Vector Store',
10314 - 'status' => 'Active',
10315 - 'documents' => $id_count > 0 ? $id_count . ' vector store' . ($id_count > 1 ? 's' : '') . ' configured' : 'No vector stores configured'
10316 - ];
10317 -
10318 - wp_send_json_success($kb_info);
10319 - return;
10320 - }
10321 -
7994 +
10322 7995 // Check Pinecone vs WordPress
10323 7996 $addon_options = get_option('mxchat_pinecone_addon_options', array());
10324 7997 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
10325 -
7998 +
10326 7999 $kb_info = [
10327 8000 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
10328 8001 'status' => 'Active'
10329 8002 ];
10330 -
8003 +
10331 8004 // Get document count
10332 8005 if ($use_pinecone) {
10333 8006 $kb_info['documents'] = 'Connected to Pinecone';
10334 8007 $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
@@ -10338,9 +8011,9 @@
10338 8011 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
10339 8012 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
10340 8013 $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
10341 8014 }
10342 -
8015 +
10343 8016 wp_send_json_success($kb_info);
10344 8017 }
10345 8018
10346 8019 /**
@@ -10422,13 +8095,9 @@
10422 8095 // Clear any other session-specific transients
10423 8096 delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
10424 8097 delete_transient("mxchat_include_pdf_in_context_{$session_id}");
10425 8098 delete_transient("mxchat_include_word_in_context_{$session_id}");
10426 -
10427 - // Clear form addon state (pending forms and submitted forms)
10428 - delete_option("mxchat_pending_form_{$session_id}");
10429 - delete_option("mxchat_submitted_forms_{$session_id}");
10430 -
8099 +
10431 8100 //error_log("MxChat: Cleared all data for session: {$session_id}");
10432 8101 }
10433 8102
10434 8103 /**
@@ -10613,17 +8282,17 @@
10613 8282 * @return string Cleaned response with invalid URLs removed/flagged
10614 8283 */
10615 8284 private function validate_and_clean_urls($response_text, $valid_urls) {
10616 8285 // DEBUG: Log what we're working with
10617 - //error_log("=== MxChat URL Validation Debug ===");
10618 - //error_log("Valid URLs count: " . count($valid_urls));
10619 - //error_log("Valid URLs: " . print_r($valid_urls, true));
10620 - //error_log("Response text length: " . strlen($response_text));
10621 - //error_log("Response text preview: " . substr($response_text, 0, 500));
8286 + error_log("=== MxChat URL Validation Debug ===");
8287 + error_log("Valid URLs count: " . count($valid_urls));
8288 + error_log("Valid URLs: " . print_r($valid_urls, true));
8289 + error_log("Response text length: " . strlen($response_text));
8290 + error_log("Response text preview: " . substr($response_text, 0, 500));
10622 8291
10623 8292 // If no valid URLs provided or empty response, return as-is
10624 8293 if (empty($valid_urls) || empty($response_text)) {
10625 - //error_log("Validation skipped - empty valid_urls or response");
8294 + error_log("Validation skipped - empty valid_urls or response");
10626 8295 return $response_text;
10627 8296 }
10628 8297
10629 8298 // Extract all URLs from the AI response
@@ -10635,9 +8304,9 @@
10635 8304 );
10636 8305
10637 8306 // If no URLs found in response, return as-is
10638 8307 if (empty($matches[0])) {
10639 - //error_log("No URLs found in response");
8308 + error_log("No URLs found in response");
10640 8309 return $response_text;
10641 8310 }
10642 8311
10643 8312 $found_urls = $matches[0];
@@ -10654,9 +8323,9 @@
10654 8323 $url = rtrim($url, '.,;:!?');
10655 8324 return $url;
10656 8325 }, $valid_urls);
10657 8326
10658 - //error_log("Normalized valid URLs: " . print_r($normalized_valid_urls, true));
8327 + error_log("Normalized valid URLs: " . print_r($normalized_valid_urls, true));
10659 8328
10660 8329 foreach ($found_urls as $found_url) {
10661 8330 // Clean up the found URL (remove trailing punctuation that might have been captured)
10662 8331 $clean_found_url = rtrim($found_url, '.,;:!?)');
@@ -10661,30 +8330,30 @@
10661 8330 // Clean up the found URL (remove trailing punctuation that might have been captured)
10662 8331 $clean_found_url = rtrim($found_url, '.,;:!?)');
10663 8332
10664 8333 // DEBUG: Log each URL being checked
10665 - //error_log("Checking found URL: " . $found_url);
8334 + error_log("Checking found URL: " . $found_url);
10666 8335
10667 8336 // Normalize for comparison
10668 8337 $normalized_found = rtrim($clean_found_url, '/');
10669 8338 $normalized_found = preg_replace('/#.*$/', '', $normalized_found);
10670 8339
10671 - //error_log("Normalized found URL: " . $normalized_found);
8340 + error_log("Normalized found URL: " . $normalized_found);
10672 8341
10673 8342 // Check if this URL exists in our valid URLs list
10674 8343 $is_valid = false;
10675 8344
10676 - //error_log("Starting validation checks for: " . $normalized_found);
8345 + error_log("Starting validation checks for: " . $normalized_found);
10677 8346
10678 8347 // First, try exact match
10679 8348 if (in_array($normalized_found, $normalized_valid_urls)) {
10680 8349 $is_valid = true;
10681 - //error_log("EXACT MATCH FOUND");
8350 + error_log("EXACT MATCH FOUND");
10682 8351 } else {
10683 - //error_log("No exact match, checking variations...");
8352 + error_log("No exact match, checking variations...");
10684 8353 // If no exact match, check if it's a variation (with query params, etc.)
10685 8354 foreach ($normalized_valid_urls as $valid_url) {
10686 - //error_log(" Comparing against valid URL: " . $valid_url);
8355 + error_log(" Comparing against valid URL: " . $valid_url);
10687 8356
10688 8357 // Check if the found URL starts with a valid URL (handles query params)
10689 8358 if (strpos($normalized_found, $valid_url) === 0) {
10690 8359 // Check what comes after the valid URL
@@ -10695,24 +8364,24 @@
10695 8364 // 2. Query params (starts with ?)
10696 8365 // 3. Fragment (starts with #)
10697 8366 if (empty($remainder) || $remainder[0] === '?' || $remainder[0] === '#') {
10698 8367 $is_valid = true;
10699 - //error_log(" MATCH: Found URL is valid variation of base URL");
8368 + error_log(" MATCH: Found URL is valid variation of base URL");
10700 8369 break;
10701 8370 } else {
10702 - //error_log(" NOT A MATCH: Found URL extends path beyond valid URL (remainder: " . $remainder . ")");
8371 + error_log(" NOT A MATCH: Found URL extends path beyond valid URL (remainder: " . $remainder . ")");
10703 8372 }
10704 8373 }
10705 8374 // Also check the reverse (in case valid URL has query params)
10706 8375 if (strpos($valid_url, $normalized_found) === 0) {
10707 8376 $is_valid = true;
10708 - //error_log(" MATCH: Valid URL starts with found URL");
8377 + error_log(" MATCH: Valid URL starts with found URL");
10709 8378 break;
10710 8379 }
10711 8380 }
10712 8381
10713 8382 if (!$is_valid) {
10714 - //error_log("NO MATCH FOUND - URL should be removed");
8383 + error_log("NO MATCH FOUND - URL should be removed");
10715 8384 }
10716 8385 }
10717 8386
10718 8387 // If URL is not valid, remove it from the response
@@ -10717,10 +8386,10 @@
10717 8386
10718 8387 // If URL is not valid, remove it from the response
10719 8388 if (!$is_valid) {
10720 8389 // Log the removal for debugging
10721 - //error_log("MxChat: Removed hallucinated URL: " . $found_url);
10722 - //error_log("MxChat: Valid URLs were: " . implode(', ', array_slice($normalized_valid_urls, 0, 5)));
8390 + error_log("MxChat: Removed hallucinated URL: " . $found_url);
8391 + error_log("MxChat: Valid URLs were: " . implode(', ', array_slice($normalized_valid_urls, 0, 5)));
10723 8392
10724 8393 $removed_count++;
10725 8394
10726 8395 // Check if URL is part of a markdown link: [text](url)
@@ -10725,15 +8394,15 @@
10725 8394
10726 8395 // Check if URL is part of a markdown link: [text](url)
10727 8396 $markdown_pattern = '/\[([^\]]+)\]\(' . preg_quote($found_url, '/') . '\)/';
10728 8397 if (preg_match($markdown_pattern, $cleaned_response)) {
10729 - //error_log("Found markdown link, removing but keeping text");
8398 + error_log("Found markdown link, removing but keeping text");
10730 8399 // Remove the markdown link but keep the text
10731 8400 $cleaned_response = preg_replace($markdown_pattern, '$1', $cleaned_response);
10732 8401 }
10733 8402 // Check if URL is part of an HTML link: <a href="url">text</a>
10734 8403 else if (preg_match('/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>(.*?)<\/a>/i', $cleaned_response, $link_match)) {
10735 - //error_log("Found HTML link, removing but keeping text");
8404 + error_log("Found HTML link, removing but keeping text");
10736 8405 // Remove the HTML link but keep the text
10737 8406 $link_text = $link_match[1];
10738 8407 $cleaned_response = preg_replace(
10739 8408 '/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>.*?<\/a>/i',
@@ -10742,9 +8411,9 @@
10742 8411 );
10743 8412 }
10744 8413 // Otherwise just remove the bare URL
10745 8414 else {
10746 - //error_log("Removing bare URL");
8415 + error_log("Removing bare URL");
10747 8416 $cleaned_response = str_replace($found_url, '', $cleaned_response);
10748 8417 }
10749 8418 }
10750 8419 }
@@ -10750,19 +8419,18 @@
10750 8419 }
10751 8420
10752 8421 // Log summary if any URLs were removed
10753 8422 if ($removed_count > 0) {
10754 - //error_log("MxChat: URL Validation Summary - Removed {$removed_count} hallucinated URL(s)");
8423 + error_log("MxChat: URL Validation Summary - Removed {$removed_count} hallucinated URL(s)");
10755 8424 } else {
10756 - //error_log("MxChat: URL Validation Summary - No URLs removed, all were valid");
8425 + error_log("MxChat: URL Validation Summary - No URLs removed, all were valid");
10757 8426 }
10758 8427
10759 8428 // Clean up any double spaces or awkward punctuation left behind
10760 - // IMPORTANT: Only collapse horizontal whitespace (spaces/tabs), preserve newlines for markdown formatting
10761 - $cleaned_response = preg_replace('/[^\S\n]+/', ' ', $cleaned_response); // Collapse spaces/tabs but NOT newlines
10762 - $cleaned_response = preg_replace('/[^\S\n]+([.,;:!?])/', '$1', $cleaned_response); // Same for punctuation cleanup
8429 + $cleaned_response = preg_replace('/\s+/', ' ', $cleaned_response);
8430 + $cleaned_response = preg_replace('/\s+([.,;:!?])/', '$1', $cleaned_response);
10763 8431
10764 - //error_log("Final cleaned response: " . $cleaned_response);
8432 + error_log("Final cleaned response: " . $cleaned_response);
10765 8433
10766 8434 return trim($cleaned_response);
10767 8435 }
10768 8436