PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.24
MxChat – AI Chatbot & Content Generation for WordPress v3.2.24
3.2.25 3.2.24 3.2.23 3.2.22 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 All 156 releases
← All changes | admin/class-pinecone-documents.php +140 -36 3.2.23 → 3.2.24 View file →
@@ -735,24 +735,33 @@
735 735 * the first run and full is the whole joined run, both un-normalised.
736 736 */
737 737 private static function code_runs($text) {
738 738 $runs = array();
739 + $text = (string) $text;
739 740 $pattern = '/(?<![A-Za-z0-9])([A-Za-z0-9]+(?:[.\-][A-Za-z0-9]+)*)((?:[ .\-]\d{2,6}(?![A-Za-z0-9])){1,3})?(?![A-Za-z0-9])/u';
740 - if (!preg_match_all($pattern, (string) $text, $m, PREG_SET_ORDER)) {
741 - return $runs;
742 - }
743 - foreach ($m as $match) {
744 - $head = rtrim($match[1], '.');
745 - if (strlen($head) < self::CODE_MIN_LEN || !preg_match('/\d/', $head)) {
741 + $offset = 0;
742 + $length = strlen($text);
743 + // 0d7bd4: walked one match at a time. Only a head that IS a code keeps
744 + // its trailing digit groups; a plain word ("part 4471-AB-12") used to
745 + // take "4471" as its tail and the digit-led number was never stored.
746 + while ($offset < $length && preg_match($pattern, $text, $match, PREG_OFFSET_CAPTURE, $offset)) {
747 + $head = rtrim($match[1][0], '.');
748 + $head_end = $match[1][1] + strlen($match[1][0]);
749 + $is_code = strlen($head) >= self::CODE_MIN_LEN && preg_match('/\d/', $head);
750 + if ($is_code) {
751 + $has_letter = (bool) preg_match('/[A-Za-z]/', $head);
752 + $separators = preg_match_all('/[.\-]/', $head);
753 + if (!$has_letter && $separators < 2) {
754 + $is_code = false; // 2024, 26.5, 10-20
755 + }
756 + }
757 + if (!$is_code) {
758 + $offset = $head_end;
746 759 continue;
747 760 }
748 - $has_letter = (bool) preg_match('/[A-Za-z]/', $head);
749 - $separators = preg_match_all('/[.\-]/', $head);
750 - if (!$has_letter && $separators < 2) {
751 - continue; // 2024, 26.5, 10-20
752 - }
753 - $tail = isset($match[2]) ? $match[2] : '';
761 + $tail = isset($match[2]) && $match[2][1] >= 0 ? $match[2][0] : '';
754 762 $runs[] = array($head, $head . $tail);
763 + $offset = $match[0][1] + strlen($match[0][0]);
755 764 }
756 765 return $runs;
757 766 }
758 767
@@ -780,9 +789,11 @@
780 789 if (count($codes) >= self::CODES_MAX) {
781 790 break;
782 791 }
783 792 }
784 - return array_slice(array_keys($codes), 0, self::CODES_MAX);
793 + // 0d7bd4: strval — PHP turns an all-digit array key into an int, and
794 + // Pinecone refuses a list that mixes strings and numbers.
795 + return array_slice(array_map('strval', array_keys($codes)), 0, self::CODES_MAX);
785 796 }
786 797
787 798 /**
788 799 * What a question's tokens expand to for the codes $in filter: the
@@ -809,9 +820,9 @@
809 820 if (count($codes) >= 200) {
810 821 break;
811 822 }
812 823 }
813 - return array_slice(array_keys($codes), 0, 200);
824 + return array_slice(array_map('strval', array_keys($codes)), 0, 200);
814 825 }
815 826
816 827 /**
817 828 * Order keyword-leg hits so an exact code match outranks a prefix-only
@@ -1089,29 +1100,55 @@
1089 1100 }
1090 1101
1091 1102 /** Dimension of the active embedding model (same table the KB screen uses). */
1092 1103 public static function expected_dimension() {
1104 + $dimension = self::expected_dimension_checked();
1105 + return is_wp_error($dimension) ? 0 : (int) $dimension;
1106 + }
1107 +
1108 + /**
1109 + * 418afd: the dimension the site's embeddings really have, or why it is
1110 + * not known. A Custom Provider embedding model is asked for its size
1111 + * (MxChat_Utils::expected_embedding_dimension()); the standard dropdown is
1112 + * disabled on such a site and used to size the index anyway. The filter
1113 + * still has the last word, and can supply a number when the model could
1114 + * not be asked.
1115 + *
1116 + * @param bool $force Ask a custom model again even after a recent failure (the card's buttons).
1117 + * @return int|WP_Error
1118 + */
1119 + public static function expected_dimension_checked($force = false) {
1093 1120 $options = get_option('mxchat_options', array());
1094 - $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
1095 - $model_dimensions = array(
1096 - 'text-embedding-ada-002' => 1536,
1097 - 'text-embedding-3-small' => 1536,
1098 - 'text-embedding-3-large' => 3072,
1099 - 'voyage-2' => 1024,
1100 - 'voyage-large-2' => 1536,
1101 - 'voyage-3-large' => 2048,
1102 - 'gemini-embedding-001' => 1536,
1103 - );
1104 - if (strpos($selected_model, 'voyage-3-large') === 0) {
1105 - return intval($options['voyage_output_dimension'] ?? 2048);
1121 + $options = is_array($options) ? $options : array();
1122 + $custom = isset($options['custom_provider_for_embeddings']) && $options['custom_provider_for_embeddings'] === 'on';
1123 + $selected_model = $custom
1124 + ? MxChat_Utils::get_selected_embedding_model($options)
1125 + : ($options['embedding_model'] ?? 'text-embedding-ada-002');
1126 + $dimension = MxChat_Utils::expected_embedding_dimension($options, $force);
1127 + if (is_wp_error($dimension)) {
1128 + $supplied = (int) apply_filters('mxchat_pinecone_expected_dimension', 0, $selected_model);
1129 + return $supplied > 0 ? $supplied : $dimension;
1106 1130 }
1107 - if (strpos($selected_model, 'gemini-embedding') === 0) {
1108 - return intval($options['gemini_output_dimension'] ?? 1536);
1131 + if (!$custom && (strpos($selected_model, 'voyage-3-large') === 0 || strpos($selected_model, 'gemini-embedding') === 0)) {
1132 + return (int) $dimension; // adjustable-size models: the owner's own setting, never filtered (as before)
1109 1133 }
1110 - $dimension = apply_filters('mxchat_pinecone_expected_dimension', $model_dimensions[$selected_model] ?? 1536, $selected_model);
1111 - return (int) $dimension;
1134 + return (int) apply_filters('mxchat_pinecone_expected_dimension', $dimension, $selected_model);
1112 1135 }
1113 1136
1137 + /** Plain-language refusal for the card when a custom model's size is unknown. (Plain __(): the card's JS text-escapes it.) */
1138 + public static function dimension_unknown_message(WP_Error $error, $action) {
1139 + $reason = trim($error->get_error_message());
1140 + $tail = $action === 'create'
1141 + ? __('The index was not created.', 'mxchat')
1142 + : __('The index was not checked.', 'mxchat');
1143 + return sprintf(
1144 + /* translators: 1: the error the embedding endpoint returned, 2: what did not happen */
1145 + __('Could not determine the dimension of your Custom Provider embedding model (%1$s). %2$s Check the Custom Provider embedding settings, then try again.', 'mxchat'),
1146 + $reason !== '' ? $reason : __('no answer from the embedding endpoint', 'mxchat'),
1147 + $tail
1148 + );
1149 + }
1150 +
1114 1151 // =====================================================================
1115 1152 // Options (raw writes — the registered sanitizer rebuilds the array and
1116 1153 // would flip mxchat_use_pinecone on any isset key; the autosave handler
1117 1154 // writes this option the same raw way for the same reason)
@@ -1497,11 +1534,13 @@
1497 1534 } else {
1498 1535 $total = 0;
1499 1536 }
1500 1537 $per_namespace[self::ns_key($namespace)] = self::new_namespace_entry($namespace, $total, 'running');
1501 - foreach ($ns_counts as $ns_name => $ns_count) {
1502 - if ($ns_name !== $namespace && $ns_count > 0) {
1503 - $other_namespaces[self::ns_key($ns_name)] = $ns_count;
1538 + // 47c61f: in the copy order (default first, then by name). Pinecone's
1539 + // stats object lists namespaces in a different order call to call.
1540 + foreach (self::namespace_queue($ns_counts) as $ns_name) {
1541 + if ((string) $ns_name !== (string) $namespace) {
1542 + $other_namespaces[self::ns_key($ns_name)] = (int) $ns_counts[$ns_name];
1504 1543 }
1505 1544 }
1506 1545 }
1507 1546 $source_dimension = (int) ($stats['dimension'] ?? 0);
@@ -1755,8 +1794,48 @@
1755 1794 implode(', ', $parts)
1756 1795 );
1757 1796 }
1758 1797
1798 + /**
1799 + * 5872ef: bots that still keep the given Pinecone host as their own (the
1800 + * Multi-Bot add-on stores a host per bot). Core knows nothing about the
1801 + * bots table — the add-on answers the filter, and with it inactive the
1802 + * list is empty. Delete old index refuses while this is not empty.
1803 + *
1804 + * @return array bot id → bot name
1805 + */
1806 + public static function bots_on_host($host) {
1807 + $host = strtolower(trim(str_replace(array('https://', 'http://'), '', (string) $host), '/'));
1808 + if ($host === '') {
1809 + return array();
1810 + }
1811 + $bots = apply_filters('mxchat_pinecone_bots_on_host', array(), $host);
1812 + if (!is_array($bots)) {
1813 + return array();
1814 + }
1815 + $named = array();
1816 + foreach ($bots as $bot_id => $name) {
1817 + $name = is_scalar($name) ? trim((string) $name) : '';
1818 + $named[(string) $bot_id] = $name !== '' ? $name : (string) $bot_id;
1819 + }
1820 + return $named;
1821 + }
1822 +
1823 + /** Plain-language refusal naming every bot still on the old index. (Plain __(): the card's JS text-escapes it.) */
1824 + public static function bots_on_host_message(array $bots) {
1825 + return sprintf(
1826 + /* translators: 1: number of bots, 2: comma-separated bot names */
1827 + _n(
1828 + '%1$s bot still uses the old index: %2$s. Move it to the new index first (Multi-Bot, All Bots, Move these bots to the new index), or it will stop answering. The old index stays.',
1829 + '%1$s bots still use the old index: %2$s. Move them to the new index first (Multi-Bot, All Bots, Move these bots to the new index), or they will stop answering. The old index stays.',
1830 + count($bots),
1831 + 'mxchat'
1832 + ),
1833 + number_format_i18n(count($bots)),
1834 + implode(', ', $bots)
1835 + );
1836 + }
1837 +
1759 1838 /** Record a step failure without ending the copy: status stays running, the cursor stays put. */
1760 1839 private static function migration_fail(array $state, $message) {
1761 1840 $state['error'] = (string) $message;
1762 1841 $state['error_at'] = time();
@@ -1842,9 +1921,13 @@
1842 1921 }
1843 1922 if ($name === '') {
1844 1923 wp_send_json_error(array('message' => esc_html__('Enter an index name first — the new document index will be created under that name.', 'mxchat')));
1845 1924 }
1846 - $dimension = self::expected_dimension();
1925 + $dimension = self::expected_dimension_checked(true);
1926 + if (is_wp_error($dimension)) {
1927 + // 418afd: never size an index from a guess
1928 + wp_send_json_error(array('message' => self::dimension_unknown_message($dimension, 'create'), 'code' => 'dimension_unknown'));
1929 + }
1847 1930 $index = self::create_document_index($name, $dimension, $cloud, $region, $language, $api_key);
1848 1931 if (is_wp_error($index)) {
1849 1932 $data = $index->get_error_data();
1850 1933 wp_send_json_error(array(
@@ -1907,9 +1990,13 @@
1907 1990 $host = trim(str_replace(array('https://', 'http://'), '', $host), '/');
1908 1991 if ($api_key === '' || $host === '') {
1909 1992 wp_send_json_error(array('message' => esc_html__('Enter the Pinecone API key and host first.', 'mxchat')));
1910 1993 }
1911 - $check = self::check_document_index($host, $api_key, self::expected_dimension());
1994 + $dimension = self::expected_dimension_checked(true);
1995 + if (is_wp_error($dimension)) {
1996 + wp_send_json_error(array('ok' => false, 'message' => self::dimension_unknown_message($dimension, 'check'), 'code' => 'dimension_unknown'));
1997 + }
1998 + $check = self::check_document_index($host, $api_key, $dimension);
1912 1999 if (!$check['ok']) {
1913 2000 wp_send_json_error($check);
1914 2001 }
1915 2002 $options = get_option(self::OPTION, array());
@@ -1959,9 +2046,13 @@
1959 2046 $target_host = self::posted_target_host($options);
1960 2047 if ($target_host === '') {
1961 2048 wp_send_json_error(array('message' => esc_html__('Enter the host of the document index to copy into (or create one with Create index for me).', 'mxchat')));
1962 2049 }
1963 - $check = self::check_document_index($target_host, $api_key, self::expected_dimension());
2050 + $dimension = self::expected_dimension_checked(true);
2051 + if (is_wp_error($dimension)) {
2052 + wp_send_json_error(array('message' => self::dimension_unknown_message($dimension, 'check'), 'code' => 'dimension_unknown'));
2053 + }
2054 + $check = self::check_document_index($target_host, $api_key, $dimension);
1964 2055 if (!$check['ok']) {
1965 2056 wp_send_json_error(array('message' => $check['message']));
1966 2057 }
1967 2058 $target = array('api_key' => $api_key, 'host' => strtolower($check['host']), 'namespace' => $namespace, 'index_type' => 'document');
@@ -2022,8 +2113,21 @@
2022 2113 // finished copy whose count matches — not just the one copied last.
2023 2114 // Before this, a Multi-Bot or legacy-namespace site could delete
2024 2115 // records that were never copied.
2025 2116 $missing = self::uncopied_namespaces($state, $target['api_key']);
2117 + // 5872ef: a bot that still keeps the old index as its own host stops
2118 + // answering the moment that index is deleted, however complete the
2119 + // copy is. Refuse and name the bots; when namespaces are uncopied too,
2120 + // say both in one answer.
2121 + $bots = self::bots_on_host((string) ($state['source_host'] ?? ''));
2122 + if (!empty($bots)) {
2123 + $refusal = array('message' => self::bots_on_host_message($bots), 'bots' => $bots);
2124 + if (is_array($missing) && !empty($missing)) {
2125 + $refusal['message'] .= ' ' . self::uncopied_message($missing);
2126 + $refusal['uncopied'] = $missing;
2127 + }
2128 + wp_send_json_error($refusal);
2129 + }
2026 2130 if (is_wp_error($missing)) {
2027 2131 wp_send_json_error(array('message' => sprintf(__('Could not read the old index before deleting it (%s). The old index stays.', 'mxchat'), $missing->get_error_message())));
2028 2132 }
2029 2133 if (!empty($missing)) {