| @@ -735,33 +735,24 @@ | ||
| 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; | |
| 740 | 739 | $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'; |
| 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 | - } | |
| 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)) { | |
| 746 | + continue; | |
| 756 | 747 | } |
| 757 | - if (!$is_code) { | |
| 758 | - $offset = $head_end; | |
| 759 | - continue; | |
| 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 | |
| 760 | 752 | } |
| 761 | - $tail = isset($match[2]) && $match[2][1] >= 0 ? $match[2][0] : ''; | |
| 753 | + $tail = isset($match[2]) ? $match[2] : ''; | |
| 762 | 754 | $runs[] = array($head, $head . $tail); |
| 763 | - $offset = $match[0][1] + strlen($match[0][0]); | |
| 764 | 755 | } |
| 765 | 756 | return $runs; |
| 766 | 757 | } |
| 767 | 758 | |
| @@ -789,11 +780,9 @@ | ||
| 789 | 780 | if (count($codes) >= self::CODES_MAX) { |
| 790 | 781 | break; |
| 791 | 782 | } |
| 792 | 783 | } |
| 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); | |
| 784 | + return array_slice(array_keys($codes), 0, self::CODES_MAX); | |
| 796 | 785 | } |
| 797 | 786 | |
| 798 | 787 | /** |
| 799 | 788 | * What a question's tokens expand to for the codes $in filter: the |
| @@ -820,9 +809,9 @@ | ||
| 820 | 809 | if (count($codes) >= 200) { |
| 821 | 810 | break; |
| 822 | 811 | } |
| 823 | 812 | } |
| 824 | - return array_slice(array_map('strval', array_keys($codes)), 0, 200); | |
| 813 | + return array_slice(array_keys($codes), 0, 200); | |
| 825 | 814 | } |
| 826 | 815 | |
| 827 | 816 | /** |
| 828 | 817 | * Order keyword-leg hits so an exact code match outranks a prefix-only |
| @@ -1100,55 +1089,29 @@ | ||
| 1100 | 1089 | } |
| 1101 | 1090 | |
| 1102 | 1091 | /** Dimension of the active embedding model (same table the KB screen uses). */ |
| 1103 | 1092 | 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) { | |
| 1120 | 1093 | $options = get_option('mxchat_options', array()); |
| 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; | |
| 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); | |
| 1130 | 1106 | } |
| 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) | |
| 1107 | + if (strpos($selected_model, 'gemini-embedding') === 0) { | |
| 1108 | + return intval($options['gemini_output_dimension'] ?? 1536); | |
| 1133 | 1109 | } |
| 1134 | - return (int) apply_filters('mxchat_pinecone_expected_dimension', $dimension, $selected_model); | |
| 1110 | + $dimension = apply_filters('mxchat_pinecone_expected_dimension', $model_dimensions[$selected_model] ?? 1536, $selected_model); | |
| 1111 | + return (int) $dimension; | |
| 1135 | 1112 | } |
| 1136 | 1113 | |
| 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 | - | |
| 1151 | 1114 | // ===================================================================== |
| 1152 | 1115 | // Options (raw writes — the registered sanitizer rebuilds the array and |
| 1153 | 1116 | // would flip mxchat_use_pinecone on any isset key; the autosave handler |
| 1154 | 1117 | // writes this option the same raw way for the same reason) |
| @@ -1534,13 +1497,11 @@ | ||
| 1534 | 1497 | } else { |
| 1535 | 1498 | $total = 0; |
| 1536 | 1499 | } |
| 1537 | 1500 | $per_namespace[self::ns_key($namespace)] = self::new_namespace_entry($namespace, $total, 'running'); |
| 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]; | |
| 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; | |
| 1543 | 1504 | } |
| 1544 | 1505 | } |
| 1545 | 1506 | } |
| 1546 | 1507 | $source_dimension = (int) ($stats['dimension'] ?? 0); |
| @@ -1794,48 +1755,8 @@ | ||
| 1794 | 1755 | implode(', ', $parts) |
| 1795 | 1756 | ); |
| 1796 | 1757 | } |
| 1797 | 1758 | |
| 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 | - | |
| 1838 | 1759 | /** Record a step failure without ending the copy: status stays running, the cursor stays put. */ |
| 1839 | 1760 | private static function migration_fail(array $state, $message) { |
| 1840 | 1761 | $state['error'] = (string) $message; |
| 1841 | 1762 | $state['error_at'] = time(); |
| @@ -1921,13 +1842,9 @@ | ||
| 1921 | 1842 | } |
| 1922 | 1843 | if ($name === '') { |
| 1923 | 1844 | wp_send_json_error(array('message' => esc_html__('Enter an index name first — the new document index will be created under that name.', 'mxchat'))); |
| 1924 | 1845 | } |
| 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 | - } | |
| 1846 | + $dimension = self::expected_dimension(); | |
| 1930 | 1847 | $index = self::create_document_index($name, $dimension, $cloud, $region, $language, $api_key); |
| 1931 | 1848 | if (is_wp_error($index)) { |
| 1932 | 1849 | $data = $index->get_error_data(); |
| 1933 | 1850 | wp_send_json_error(array( |
| @@ -1990,13 +1907,9 @@ | ||
| 1990 | 1907 | $host = trim(str_replace(array('https://', 'http://'), '', $host), '/'); |
| 1991 | 1908 | if ($api_key === '' || $host === '') { |
| 1992 | 1909 | wp_send_json_error(array('message' => esc_html__('Enter the Pinecone API key and host first.', 'mxchat'))); |
| 1993 | 1910 | } |
| 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); | |
| 1911 | + $check = self::check_document_index($host, $api_key, self::expected_dimension()); | |
| 1999 | 1912 | if (!$check['ok']) { |
| 2000 | 1913 | wp_send_json_error($check); |
| 2001 | 1914 | } |
| 2002 | 1915 | $options = get_option(self::OPTION, array()); |
| @@ -2046,13 +1959,9 @@ | ||
| 2046 | 1959 | $target_host = self::posted_target_host($options); |
| 2047 | 1960 | if ($target_host === '') { |
| 2048 | 1961 | 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'))); |
| 2049 | 1962 | } |
| 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); | |
| 1963 | + $check = self::check_document_index($target_host, $api_key, self::expected_dimension()); | |
| 2055 | 1964 | if (!$check['ok']) { |
| 2056 | 1965 | wp_send_json_error(array('message' => $check['message'])); |
| 2057 | 1966 | } |
| 2058 | 1967 | $target = array('api_key' => $api_key, 'host' => strtolower($check['host']), 'namespace' => $namespace, 'index_type' => 'document'); |
| @@ -2113,21 +2022,8 @@ | ||
| 2113 | 2022 | // finished copy whose count matches — not just the one copied last. |
| 2114 | 2023 | // Before this, a Multi-Bot or legacy-namespace site could delete |
| 2115 | 2024 | // records that were never copied. |
| 2116 | 2025 | $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 | - } | |
| 2130 | 2026 | if (is_wp_error($missing)) { |
| 2131 | 2027 | 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()))); |
| 2132 | 2028 | } |
| 2133 | 2029 | if (!empty($missing)) { |