| @@ -4,112 +4,485 @@ | ||
| 4 | 4 | } |
| 5 | 5 | |
| 6 | 6 | class MxChat_Utils { |
| 7 | 7 | |
| 8 | - /** | |
| 9 | - * Submit or update content (and its embedding) in the database. | |
| 10 | - * | |
| 11 | - * If the source_url already exists, update that row. | |
| 12 | - * Otherwise, insert a new row. | |
| 13 | - * | |
| 14 | - * @param string $content The content to be embedded. | |
| 15 | - * @param string $source_url The source URL of the content. | |
| 16 | - * @param string $api_key The API key used for generating embeddings. | |
| 17 | - */ | |
| 18 | - public static function submit_content_to_db($content, $source_url, $api_key) { | |
| 19 | - global $wpdb; | |
| 20 | - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; | |
| 8 | +/** | |
| 9 | + * UPDATED: Submit or update content (and its embedding) in the database. | |
| 10 | + * Stores in Pinecone if enabled, otherwise stores in WordPress DB. | |
| 11 | + * | |
| 12 | + * @param string $content The content to be embedded. | |
| 13 | + * @param string $source_url The source URL of the content. | |
| 14 | + * @param string $api_key The API key used for generating embeddings. | |
| 15 | + * @param string $vector_id Optional vector ID for Pinecone (if not provided, will use md5 of URL) | |
| 16 | + * @param string $bot_id The bot ID for multi-bot support | |
| 17 | + * @return bool|WP_Error True on success, WP_Error on failure | |
| 18 | + */ | |
| 19 | +public static function submit_content_to_db($content, $source_url, $api_key, $vector_id = null, $bot_id = 'default') { | |
| 20 | + global $wpdb; | |
| 21 | + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; | |
| 22 | + | |
| 23 | + //error_log('[MXCHAT-DB] Starting database submission for URL: ' . $source_url . ' (Bot: ' . $bot_id . ')'); | |
| 24 | + //error_log('[MXCHAT-DB] Content length: ' . strlen($content) . ' bytes'); | |
| 25 | + | |
| 26 | + // Sanitize the source URL | |
| 27 | + $source_url = esc_url_raw($source_url); | |
| 28 | + | |
| 29 | + // Just ensure UTF-8 validity without aggressive escaping | |
| 30 | + $safe_content = wp_check_invalid_utf8($content); | |
| 31 | + // Remove only null bytes and other control characters, but preserve newlines (\n = \x0A) and carriage returns (\r = \x0D) | |
| 32 | + $safe_content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $safe_content); | |
| 21 | 33 | |
| 22 | - // Sanitize the source URL | |
| 23 | - $source_url = esc_url_raw($source_url); | |
| 34 | + // UPDATED: Generate the embedding using bot-specific configuration | |
| 35 | + $embedding_vector = self::generate_embedding($content, $api_key, $bot_id); | |
| 36 | + | |
| 37 | + if (!is_array($embedding_vector)) { | |
| 38 | + //error_log('[MXCHAT-DB] Error: Embedding generation failed'); | |
| 39 | + return new WP_Error('embedding_failed', 'Failed to generate embedding for content'); | |
| 40 | + } | |
| 41 | + | |
| 42 | + //error_log('[MXCHAT-DB] Embedding generated successfully'); | |
| 43 | + | |
| 44 | + // UPDATED: Check if Pinecone is enabled for this specific bot | |
| 45 | + if (self::is_pinecone_enabled_for_bot($bot_id)) { | |
| 46 | + //error_log('[MXCHAT-DB] Pinecone is enabled for bot ' . $bot_id . ' - using Pinecone storage'); | |
| 47 | + // Store in Pinecone only | |
| 48 | + return self::store_in_pinecone_only($embedding_vector, $content, $source_url, $vector_id, $bot_id); | |
| 49 | + } else { | |
| 50 | + //error_log('[MXCHAT-DB] Pinecone not enabled for bot ' . $bot_id . ' - using WordPress storage'); | |
| 51 | + // Store in WordPress database only | |
| 52 | + $embedding_vector_serialized = maybe_serialize($embedding_vector); | |
| 53 | + return self::store_in_wordpress_db($safe_content, $source_url, $embedding_vector_serialized, $table_name); | |
| 54 | + } | |
| 55 | +} | |
| 24 | 56 | |
| 25 | - // Generate the embedding using the API key | |
| 26 | - $embedding_vector = self::generate_embedding($content, $api_key); | |
| 57 | +/** | |
| 58 | + * UPDATED: Check if Pinecone is enabled and properly configured for a specific bot | |
| 59 | + */ | |
| 60 | +private static function is_pinecone_enabled_for_bot($bot_id = 'default') { | |
| 61 | + // For default bot or when multi-bot is not active, use original method | |
| 62 | + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) { | |
| 63 | + return self::is_pinecone_enabled(); | |
| 64 | + } | |
| 65 | + | |
| 66 | + // Get bot-specific Pinecone configuration | |
| 67 | + $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id); | |
| 68 | + | |
| 69 | + if (empty($bot_pinecone_config)) { | |
| 70 | + // Fallback to default configuration | |
| 71 | + return self::is_pinecone_enabled(); | |
| 72 | + } | |
| 73 | + | |
| 74 | + $enabled_check = !empty($bot_pinecone_config['use_pinecone']) && $bot_pinecone_config['use_pinecone']; | |
| 75 | + $api_key_check = !empty($bot_pinecone_config['api_key']); | |
| 76 | + $host_check = !empty($bot_pinecone_config['host']); | |
| 77 | + | |
| 78 | + return $enabled_check && $api_key_check && $host_check; | |
| 79 | +} | |
| 27 | 80 | |
| 28 | - if (is_array($embedding_vector)) { | |
| 29 | - $embedding_vector_serialized = maybe_serialize($embedding_vector); | |
| 81 | +/** | |
| 82 | + * Check if Pinecone is enabled and properly configured (original method for default bot) | |
| 83 | + */ | |
| 84 | +private static function is_pinecone_enabled() { | |
| 85 | + $pinecone_options = get_option('mxchat_pinecone_addon_options'); | |
| 86 | + | |
| 87 | + if (empty($pinecone_options)) { | |
| 88 | + return false; | |
| 89 | + } | |
| 90 | + | |
| 91 | + $enabled_check = !empty($pinecone_options['mxchat_use_pinecone']) && $pinecone_options['mxchat_use_pinecone'] !== '0'; | |
| 92 | + $api_key_check = !empty($pinecone_options['mxchat_pinecone_api_key']); | |
| 93 | + $host_check = !empty($pinecone_options['mxchat_pinecone_host']); | |
| 94 | + | |
| 95 | + return $enabled_check && $api_key_check && $host_check; | |
| 96 | +} | |
| 30 | 97 | |
| 31 | - // Check if a row already exists for this URL | |
| 32 | - $existing_id = $wpdb->get_var( | |
| 33 | - $wpdb->prepare( | |
| 34 | - "SELECT id FROM {$table_name} WHERE source_url = %s LIMIT 1", | |
| 35 | - $source_url | |
| 36 | - ) | |
| 37 | - ); | |
| 98 | +/** | |
| 99 | + * UPDATED: Store content in Pinecone only with bot support | |
| 100 | + */ | |
| 101 | +private static function store_in_pinecone_only($embedding_vector, $content, $source_url, $vector_id = null, $bot_id = 'default') { | |
| 102 | + //error_log('[MXCHAT-PINECONE] ===== Using Pinecone-only storage for bot ' . $bot_id . ' ====='); | |
| 103 | + | |
| 104 | + // Get bot-specific Pinecone configuration | |
| 105 | + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) { | |
| 106 | + $pinecone_options = get_option('mxchat_pinecone_addon_options'); | |
| 107 | + $api_key = $pinecone_options['mxchat_pinecone_api_key']; | |
| 108 | + $environment = $pinecone_options['mxchat_pinecone_environment'] ?? ''; | |
| 109 | + $index_name = $pinecone_options['mxchat_pinecone_index'] ?? ''; | |
| 110 | + $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; | |
| 111 | + } else { | |
| 112 | + $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id); | |
| 113 | + if (empty($bot_pinecone_config)) { | |
| 114 | + // Fallback to default configuration | |
| 115 | + $pinecone_options = get_option('mxchat_pinecone_addon_options'); | |
| 116 | + $api_key = $pinecone_options['mxchat_pinecone_api_key']; | |
| 117 | + $environment = $pinecone_options['mxchat_pinecone_environment'] ?? ''; | |
| 118 | + $index_name = $pinecone_options['mxchat_pinecone_index'] ?? ''; | |
| 119 | + $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; | |
| 120 | + } else { | |
| 121 | + $api_key = $bot_pinecone_config['api_key']; | |
| 122 | + $environment = ''; // Not used in new Pinecone API | |
| 123 | + $index_name = ''; // Not used in new Pinecone API | |
| 124 | + $namespace = $bot_pinecone_config['namespace'] ?? ''; | |
| 125 | + } | |
| 126 | + } | |
| 127 | + | |
| 128 | + $result = self::store_in_pinecone_main( | |
| 129 | + $embedding_vector, | |
| 130 | + $content, | |
| 131 | + $source_url, | |
| 132 | + $api_key, | |
| 133 | + $environment, | |
| 134 | + $index_name, | |
| 135 | + $vector_id, | |
| 136 | + $bot_id, | |
| 137 | + $namespace | |
| 138 | + ); | |
| 139 | + | |
| 140 | + if (is_wp_error($result)) { | |
| 141 | + //error_log('[MXCHAT-PINECONE] Pinecone storage failed for bot ' . $bot_id . ': ' . $result->get_error_message()); | |
| 142 | + return $result; | |
| 143 | + } | |
| 144 | + | |
| 145 | + //error_log('[MXCHAT-PINECONE] Pinecone storage completed successfully for bot ' . $bot_id); | |
| 146 | + return true; | |
| 147 | +} | |
| 38 | 148 | |
| 149 | +/** | |
| 150 | + * Store content in WordPress database with progressive fallback (unchanged) | |
| 151 | + */ | |
| 152 | +private static function store_in_wordpress_db($safe_content, $source_url, $embedding_vector_serialized, $table_name) { | |
| 153 | + global $wpdb; | |
| 154 | + | |
| 155 | + //error_log('[MXCHAT-DB] ===== Using WordPress-only storage ====='); | |
| 156 | + | |
| 157 | + // ===== FIXED: Generate unique identifier for manual content ===== | |
| 158 | + $original_source_url = $source_url; | |
| 159 | + $is_manual_content = empty($source_url) || $source_url === '' || !filter_var($source_url, FILTER_VALIDATE_URL); | |
| 160 | + | |
| 161 | + if ($is_manual_content) { | |
| 162 | + // Generate unique identifier for manual content to prevent overwrites | |
| 163 | + $source_url = 'mxchat://manual-content/' . time() . '-' . wp_generate_password(8, false); | |
| 164 | + //error_log('[MXCHAT-DB] Generated unique ID for manual content: ' . $source_url); | |
| 165 | + } | |
| 166 | + | |
| 167 | + // Only check for duplicates if we have a valid source URL (not manual content) | |
| 168 | + $existing_id = null; | |
| 169 | + if (!$is_manual_content) { | |
| 170 | + $existing_id = $wpdb->get_var( | |
| 171 | + $wpdb->prepare( | |
| 172 | + "SELECT id FROM {$table_name} WHERE source_url = %s LIMIT 1", | |
| 173 | + $source_url | |
| 174 | + ) | |
| 175 | + ); | |
| 176 | + //error_log('[MXCHAT-DB] Checked for existing URL, found ID: ' . ($existing_id ?: 'none')); | |
| 177 | + } else { | |
| 178 | + //error_log('[MXCHAT-DB] Manual content - will create new entry (no duplicate check)'); | |
| 179 | + } | |
| 180 | + // ===== END FIX ===== | |
| 181 | + | |
| 182 | + // Progressive fallback mechanism for problematic content | |
| 183 | + $attempt = 1; | |
| 184 | + $max_attempts = 3; | |
| 185 | + $current_content = $safe_content; | |
| 186 | + $result = false; | |
| 187 | + | |
| 188 | + while ($attempt <= $max_attempts && $result === false) { | |
| 189 | + try { | |
| 39 | 190 | if ($existing_id) { |
| 191 | + //error_log('[MXCHAT-DB] Found existing entry (ID: ' . $existing_id . '). Updating... (Attempt ' . $attempt . ')'); | |
| 192 | + | |
| 40 | 193 | // Update the existing row |
| 41 | - $wpdb->update( | |
| 194 | + $result = $wpdb->update( | |
| 42 | 195 | $table_name, |
| 43 | 196 | array( |
| 44 | - 'article_content' => $content, | |
| 197 | + 'url' => $source_url, | |
| 198 | + 'article_content' => $current_content, | |
| 45 | 199 | 'embedding_vector' => $embedding_vector_serialized, |
| 46 | - 'timestamp' => current_time('mysql'), // Remove if your table doesn't have a timestamp column | |
| 200 | + 'source_url' => $source_url, | |
| 201 | + 'timestamp' => current_time('mysql'), | |
| 47 | 202 | ), |
| 48 | 203 | array('id' => $existing_id), |
| 49 | - array('%s','%s','%s'), | |
| 204 | + array('%s','%s','%s','%s','%s'), | |
| 50 | 205 | array('%d') |
| 51 | 206 | ); |
| 52 | 207 | } else { |
| 53 | - // Insert a new row | |
| 54 | - $wpdb->insert( | |
| 208 | + //error_log('[MXCHAT-DB] No existing entry found. Inserting new row... (Attempt ' . $attempt . ')'); | |
| 209 | + //error_log('[MXCHAT-DB] Content sample: ' . substr($current_content, 0, 1000)); | |
| 210 | + | |
| 211 | + // Insert a new row (using generated unique ID for manual content) | |
| 212 | + $result = $wpdb->insert( | |
| 55 | 213 | $table_name, |
| 56 | 214 | array( |
| 57 | - 'article_content' => $content, | |
| 215 | + 'url' => $source_url, // Now unique for manual content | |
| 216 | + 'article_content' => $current_content, | |
| 58 | 217 | 'embedding_vector' => $embedding_vector_serialized, |
| 59 | - 'source_url' => $source_url, | |
| 60 | - 'timestamp' => current_time('mysql'), // Remove if your table doesn't have a timestamp column | |
| 218 | + 'source_url' => $source_url, // Now unique for manual content | |
| 219 | + 'timestamp' => current_time('mysql'), | |
| 61 | 220 | ), |
| 62 | - array('%s','%s','%s','%s') | |
| 221 | + array('%s','%s','%s','%s','%s') | |
| 63 | 222 | ); |
| 64 | 223 | } |
| 224 | + | |
| 225 | + if ($result === false) { | |
| 226 | + //error_log('[MXCHAT-DB] Database operation failed (Attempt ' . $attempt . '): ' . $wpdb->last_error); | |
| 227 | + | |
| 228 | + // Progressively apply more aggressive sanitization on failure | |
| 229 | + if ($attempt === 1) { | |
| 230 | + // First fallback: Use a more aggressive character filter and shorten | |
| 231 | + $current_content = preg_replace('/[^\p{L}\p{N}\s.,;:!?()-]/u', '', $current_content); | |
| 232 | + $current_content = substr($current_content, 0, 50000); | |
| 233 | + } else if ($attempt === 2) { | |
| 234 | + // Second fallback: Keep only alphanumeric and basic punctuation, shorten further | |
| 235 | + $current_content = preg_replace('/[^a-zA-Z0-9\s.,;:!?()-]/u', '', $current_content); | |
| 236 | + $current_content = substr($current_content, 0, 30000); | |
| 237 | + } | |
| 238 | + | |
| 239 | + $attempt++; | |
| 240 | + } | |
| 241 | + } catch (Exception $e) { | |
| 242 | + //error_log('[MXCHAT-DB] Exception during database operation: ' . $e->getMessage()); | |
| 243 | + $attempt++; | |
| 244 | + } | |
| 245 | + } | |
| 246 | + | |
| 247 | + if ($result === false) { | |
| 248 | + //error_log('[MXCHAT-DB] All database operation attempts failed'); | |
| 249 | + return new WP_Error('database_failed', 'Failed to store content in WordPress database after ' . $max_attempts . ' attempts'); | |
| 250 | + } | |
| 251 | + | |
| 252 | + //error_log('[MXCHAT-DB] WordPress database operation completed successfully (Attempt ' . ($attempt - 1) . ')'); | |
| 253 | + return true; | |
| 254 | +} | |
| 255 | + | |
| 256 | +/** | |
| 257 | + * UPDATED: Store content in Pinecone database with bot support | |
| 258 | + */ | |
| 259 | +private static function store_in_pinecone_main($embedding_vector, $content, $url, $api_key, $environment, $index_name, $vector_id = null, $bot_id = 'default', $namespace = '') { | |
| 260 | + //error_log('[MXCHAT-PINECONE-MAIN] ===== Starting Pinecone storage for bot ' . $bot_id . ' ====='); | |
| 261 | + | |
| 262 | + // ===== UPDATED: Handle manual content with unique vector IDs ===== | |
| 263 | + if ($vector_id) { | |
| 264 | + // Use provided vector ID | |
| 265 | + //error_log('[MXCHAT-PINECONE-MAIN] Using provided vector ID: ' . $vector_id); | |
| 266 | + } elseif (!empty($url) && filter_var($url, FILTER_VALIDATE_URL)) { | |
| 267 | + // For valid URLs, use URL-based ID (existing behavior) | |
| 268 | + $vector_id = md5($url); | |
| 269 | + //error_log('[MXCHAT-PINECONE-MAIN] Generated vector ID from URL: ' . $vector_id); | |
| 270 | + } else { | |
| 271 | + // For manual content (empty/invalid URL), generate unique ID | |
| 272 | + $vector_id = 'manual_' . time() . '_' . substr(md5($content . microtime(true)), 0, 8); | |
| 273 | + //error_log('[MXCHAT-PINECONE-MAIN] Generated unique vector ID for manual content: ' . $vector_id); | |
| 274 | + } | |
| 275 | + // ===== END UPDATE ===== | |
| 276 | + | |
| 277 | + // Get host from bot-specific config or fallback to default | |
| 278 | + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) { | |
| 279 | + $options = get_option('mxchat_pinecone_addon_options'); | |
| 280 | + $host = $options['mxchat_pinecone_host'] ?? ''; | |
| 281 | + } else { | |
| 282 | + $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id); | |
| 283 | + if (!empty($bot_pinecone_config)) { | |
| 284 | + $host = $bot_pinecone_config['host'] ?? ''; | |
| 65 | 285 | } else { |
| 66 | - // If embedding generation failed, you could log or handle it here | |
| 67 | - // error_log('Embedding generation failed for content from ' . $source_url); | |
| 286 | + $options = get_option('mxchat_pinecone_addon_options'); | |
| 287 | + $host = $options['mxchat_pinecone_host'] ?? ''; | |
| 68 | 288 | } |
| 69 | 289 | } |
| 290 | + | |
| 291 | + //error_log('[MXCHAT-PINECONE-MAIN] Host: ' . $host); | |
| 292 | + //error_log('[MXCHAT-PINECONE-MAIN] API key length: ' . strlen($api_key)); | |
| 293 | + //error_log('[MXCHAT-PINECONE-MAIN] Bot ID: ' . $bot_id); | |
| 294 | + //error_log('[MXCHAT-PINECONE-MAIN] Namespace: ' . $namespace); | |
| 70 | 295 | |
| 71 | - /** | |
| 72 | - * Generate an embedding for the given text using the specified API key. | |
| 73 | - * | |
| 74 | - * @param string $text The text to be embedded. | |
| 75 | - * @param string $api_key The API key used for generating embeddings. | |
| 76 | - * @return array|null The embedding vector or null on failure. | |
| 77 | - */ | |
| 78 | - private static function generate_embedding($text, $api_key) { | |
| 79 | - $endpoint = 'https://api.openai.com/v1/embeddings'; | |
| 296 | + if (empty($host)) { | |
| 297 | + //error_log('[MXCHAT-PINECONE-MAIN] ERROR: Host is empty'); | |
| 298 | + return new WP_Error('pinecone_config', 'Pinecone host is not configured. Please set the host in your bot settings.'); | |
| 299 | + } | |
| 80 | 300 | |
| 81 | - $body = wp_json_encode([ | |
| 82 | - 'input' => $text, | |
| 83 | - 'model' => 'text-embedding-ada-002' | |
| 84 | - ]); | |
| 301 | + // ===== UPDATED: Determine content type more accurately ===== | |
| 302 | + $is_product = false; | |
| 303 | + $content_type = 'manual'; // Default for manual content | |
| 304 | + | |
| 305 | + if (!empty($url) && filter_var($url, FILTER_VALIDATE_URL)) { | |
| 306 | + $is_product = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false); | |
| 307 | + $content_type = $is_product ? 'product' : 'content'; | |
| 308 | + } | |
| 309 | + | |
| 310 | + //error_log('[MXCHAT-PINECONE-MAIN] Content type: ' . $content_type); | |
| 311 | + // ===== END UPDATE ===== | |
| 85 | 312 | |
| 86 | - $args = [ | |
| 87 | - 'body' => $body, | |
| 88 | - 'headers' => [ | |
| 89 | - 'Content-Type' => 'application/json', | |
| 90 | - 'Authorization' => 'Bearer ' . $api_key, | |
| 313 | + $api_endpoint = "https://{$host}/vectors/upsert"; | |
| 314 | + //error_log('[MXCHAT-PINECONE-MAIN] API endpoint: ' . $api_endpoint); | |
| 315 | + | |
| 316 | + // UPDATED: Add bot_id to metadata and handle namespace | |
| 317 | + $metadata = array( | |
| 318 | + 'text' => $content, | |
| 319 | + 'source_url' => $url, // Can be empty for manual content | |
| 320 | + 'type' => $content_type, // 'manual', 'content', or 'product' | |
| 321 | + 'last_updated' => time(), | |
| 322 | + 'created_at' => time(), // Add creation timestamp | |
| 323 | + 'bot_id' => $bot_id // Add bot identification | |
| 324 | + ); | |
| 325 | + | |
| 326 | + $vector_data = array( | |
| 327 | + 'id' => $vector_id, | |
| 328 | + 'values' => $embedding_vector, | |
| 329 | + 'metadata' => $metadata | |
| 330 | + ); | |
| 331 | + | |
| 332 | + $request_body = array( | |
| 333 | + 'vectors' => array($vector_data) | |
| 334 | + ); | |
| 335 | + | |
| 336 | + // Add namespace if specified for multi-bot separation | |
| 337 | + if (!empty($namespace)) { | |
| 338 | + $request_body['namespace'] = $namespace; | |
| 339 | + //error_log('[MXCHAT-PINECONE-MAIN] Using namespace: ' . $namespace); | |
| 340 | + } | |
| 341 | + | |
| 342 | + //error_log('[MXCHAT-PINECONE-MAIN] Request body prepared (embedding dimensions: ' . count($embedding_vector) . ')'); | |
| 343 | + | |
| 344 | + $response = wp_remote_post($api_endpoint, array( | |
| 345 | + 'headers' => array( | |
| 346 | + 'Api-Key' => $api_key, | |
| 347 | + 'accept' => 'application/json', | |
| 348 | + 'content-type' => 'application/json' | |
| 349 | + ), | |
| 350 | + 'body' => wp_json_encode($request_body), | |
| 351 | + 'timeout' => 30, | |
| 352 | + 'data_format' => 'body' | |
| 353 | + )); | |
| 354 | + | |
| 355 | + if (is_wp_error($response)) { | |
| 356 | + //error_log('[MXCHAT-PINECONE-MAIN] WordPress request error: ' . $response->get_error_message()); | |
| 357 | + return new WP_Error('pinecone_request', $response->get_error_message()); | |
| 358 | + } | |
| 359 | + | |
| 360 | + $response_code = wp_remote_retrieve_response_code($response); | |
| 361 | + //error_log('[MXCHAT-PINECONE-MAIN] Response code: ' . $response_code); | |
| 362 | + | |
| 363 | + if ($response_code !== 200) { | |
| 364 | + $body = wp_remote_retrieve_body($response); | |
| 365 | + //error_log('[MXCHAT-PINECONE-MAIN] API error - Response body: ' . $body); | |
| 366 | + return new WP_Error('pinecone_api', sprintf( | |
| 367 | + 'Pinecone API error (HTTP %d): %s', | |
| 368 | + $response_code, | |
| 369 | + $body | |
| 370 | + )); | |
| 371 | + } | |
| 372 | + | |
| 373 | + $response_body = wp_remote_retrieve_body($response); | |
| 374 | + //error_log('[MXCHAT-PINECONE-MAIN] Success response: ' . $response_body); | |
| 375 | + //error_log('[MXCHAT-PINECONE-MAIN] Successfully stored in Pinecone for bot ' . $bot_id); | |
| 376 | + //error_log('[MXCHAT-PINECONE-MAIN] ===== Pinecone storage complete ====='); | |
| 377 | + | |
| 378 | + return true; | |
| 379 | +} | |
| 380 | + | |
| 381 | +/** | |
| 382 | + * UPDATED: Generate an embedding for the given text using bot-specific configuration. | |
| 383 | + * | |
| 384 | + * @param string $text The text to be embedded. | |
| 385 | + * @param string $api_key The API key used for generating embeddings. | |
| 386 | + * @param string $bot_id The bot ID for multi-bot support | |
| 387 | + * @return array|null The embedding vector or null on failure. | |
| 388 | + */ | |
| 389 | +private static function generate_embedding($text, $api_key, $bot_id = 'default') { | |
| 390 | + // Get bot-specific options | |
| 391 | + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) { | |
| 392 | + $options = get_option('mxchat_options'); | |
| 393 | + } else { | |
| 394 | + $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id); | |
| 395 | + $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options'); | |
| 396 | + } | |
| 397 | + | |
| 398 | + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; | |
| 399 | + | |
| 400 | + // Determine endpoint and API key based on model | |
| 401 | + if (strpos($selected_model, 'voyage') === 0) { | |
| 402 | + $endpoint = 'https://api.voyageai.com/v1/embeddings'; | |
| 403 | + $api_key = $options['voyage_api_key'] ?? ''; | |
| 404 | + } elseif (strpos($selected_model, 'gemini-embedding') === 0) { | |
| 405 | + $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent'; | |
| 406 | + $api_key = $options['gemini_api_key'] ?? ''; | |
| 407 | + } else { | |
| 408 | + $endpoint = 'https://api.openai.com/v1/embeddings'; | |
| 409 | + // Use the bot-specific API key or fallback to passed API key | |
| 410 | + $api_key = $options['api_key'] ?? $api_key; | |
| 411 | + } | |
| 412 | + | |
| 413 | + // Prepare request body based on provider | |
| 414 | + if (strpos($selected_model, 'gemini-embedding') === 0) { | |
| 415 | + // Gemini API format | |
| 416 | + $request_body = [ | |
| 417 | + 'model' => 'models/' . $selected_model, | |
| 418 | + 'content' => [ | |
| 419 | + 'parts' => [ | |
| 420 | + ['text' => $text] | |
| 421 | + ] | |
| 91 | 422 | ], |
| 92 | - 'timeout' => 60, | |
| 93 | - 'redirection' => 5, | |
| 94 | - 'blocking' => true, | |
| 95 | - 'httpversion' => '1.0', | |
| 96 | - 'sslverify' => true, | |
| 423 | + 'outputDimensionality' => 1536 | |
| 97 | 424 | ]; |
| 98 | - | |
| 99 | - $response = wp_remote_post($endpoint, $args); | |
| 100 | - | |
| 101 | - if (is_wp_error($response)) { | |
| 102 | - // error_log('Error generating embedding: ' . $response->get_error_message()); | |
| 425 | + | |
| 426 | + // Prepare headers for Gemini (API key as query parameter) | |
| 427 | + $endpoint .= '?key=' . $api_key; | |
| 428 | + $headers = [ | |
| 429 | + 'Content-Type' => 'application/json' | |
| 430 | + ]; | |
| 431 | + } else { | |
| 432 | + // OpenAI/Voyage API format | |
| 433 | + $request_body = [ | |
| 434 | + 'input' => $text, | |
| 435 | + 'model' => $selected_model | |
| 436 | + ]; | |
| 437 | + | |
| 438 | + // Add output_dimension for voyage-3-large | |
| 439 | + if ($selected_model === 'voyage-3-large') { | |
| 440 | + $request_body['output_dimension'] = 2048; | |
| 441 | + } | |
| 442 | + | |
| 443 | + // Prepare headers for OpenAI/Voyage | |
| 444 | + $headers = [ | |
| 445 | + 'Content-Type' => 'application/json', | |
| 446 | + 'Authorization' => 'Bearer ' . $api_key | |
| 447 | + ]; | |
| 448 | + } | |
| 449 | + | |
| 450 | + $args = [ | |
| 451 | + 'body' => wp_json_encode($request_body), | |
| 452 | + 'headers' => $headers, | |
| 453 | + 'timeout' => 60, | |
| 454 | + 'redirection' => 5, | |
| 455 | + 'blocking' => true, | |
| 456 | + 'httpversion' => '1.0', | |
| 457 | + 'sslverify' => true, | |
| 458 | + ]; | |
| 459 | + | |
| 460 | + $response = wp_remote_post($endpoint, $args); | |
| 461 | + | |
| 462 | + if (is_wp_error($response)) { | |
| 463 | + //error_log('Error generating embedding for bot ' . $bot_id . ': ' . $response->get_error_message()); | |
| 464 | + return null; | |
| 465 | + } | |
| 466 | + | |
| 467 | + $response_body = json_decode(wp_remote_retrieve_body($response), true); | |
| 468 | + | |
| 469 | + // Handle different response formats based on provider | |
| 470 | + if (strpos($selected_model, 'gemini-embedding') === 0) { | |
| 471 | + // Gemini API response format | |
| 472 | + if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) { | |
| 473 | + return $response_body['embedding']['values']; | |
| 474 | + } else { | |
| 475 | + //error_log('Invalid response received from Gemini embedding API for bot ' . $bot_id . ': ' . wp_json_encode($response_body)); | |
| 103 | 476 | return null; |
| 104 | 477 | } |
| 105 | - | |
| 106 | - $response_body = json_decode(wp_remote_retrieve_body($response), true); | |
| 107 | - | |
| 478 | + } else { | |
| 479 | + // OpenAI/Voyage API response format | |
| 108 | 480 | if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) { |
| 109 | 481 | return $response_body['data'][0]['embedding']; |
| 110 | 482 | } else { |
| 111 | - // error_log('Invalid response received from embedding API.'); | |
| 483 | + //error_log('Invalid response received from embedding API for bot ' . $bot_id . ': ' . wp_json_encode($response_body)); | |
| 112 | 484 | return null; |
| 113 | 485 | } |
| 114 | 486 | } |
| 115 | 487 | } |
| 488 | +} | |