array(), 'total' => 0, 'total_in_database' => 0, 'showing_recent_only' => false); } try { // Get total count for the banner message (bot-specific) - lightweight call $total_in_database = $this->mxchat_get_pinecone_total_count($pinecone_options, $bot_id); // For large databases, use optimized paginated fetching // Only fetch what we need for the current page, not all 1K records if ($total_in_database > 500) { $result = $this->mxchat_fetch_pinecone_page_optimized($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type); $result['total_in_database'] = $total_in_database; $result['showing_recent_only'] = true; // Always show banner when we're limiting results return $result; } // For smaller databases, use the existing approach but with safety limits $all_records = $this->mxchat_get_recent_entries_safe($pinecone_options, $bot_id, 500); // Filter by content type if provided if (!empty($content_type)) { $all_records = array_filter($all_records, function($record) use ($content_type) { $record_type = $record->type ?? 'content'; return $record_type === $content_type; }); $all_records = array_values($all_records); // Re-index array } // Filter by search query if provided if (!empty($search_query)) { $all_records = array_filter($all_records, function($record) use ($search_query) { $content = $record->article_content ?? ''; $source_url = $record->source_url ?? ''; return stripos($content, $search_query) !== false || stripos($source_url, $search_query) !== false; }); $all_records = array_values($all_records); // Re-index array } // UPDATED 2.6.3: Group records by source_url for chunk-aware pagination // This ensures pagination shows X entries per page, not X chunks $grouped_by_url = array(); $empty_url_records = array(); foreach ($all_records as $record) { $source_url = $record->source_url ?? ''; if (empty($source_url)) { $empty_url_records[] = $record; } else { if (!isset($grouped_by_url[$source_url])) { $grouped_by_url[$source_url] = array(); } $grouped_by_url[$source_url][] = $record; } } // Count unique entries (unique URLs + individual empty-URL records) $total_unique_entries = count($grouped_by_url) + count($empty_url_records); // Paginate by unique entries $offset = ($page - 1) * $per_page; // Build ordered list of URL groups (newest first based on first record) $url_groups_ordered = array_keys($grouped_by_url); // Get the URLs for this page $page_urls = array_slice($url_groups_ordered, $offset, $per_page); // Collect all records for this page's URLs $paged_records = array(); foreach ($page_urls as $url) { foreach ($grouped_by_url[$url] as $record) { $paged_records[] = $record; } } // Add empty-URL records if they fall within this page's range $remaining_slots = $per_page - count($page_urls); $empty_offset = max(0, $offset - count($grouped_by_url)); if ($remaining_slots > 0 && $empty_offset < count($empty_url_records)) { $empty_page_records = array_slice($empty_url_records, $empty_offset, $remaining_slots); $paged_records = array_merge($paged_records, $empty_page_records); } return array( 'data' => $paged_records, 'total' => $total_unique_entries, 'total_in_database' => $total_in_database, 'showing_recent_only' => ($total_in_database > 500) ); } catch (Exception $e) { //error_log('MxChat Pinecone fetch error: ' . $e->getMessage()); MxChat_Admin::mxchat_log_debug('pinecone_error', 'Pinecone fetch error: ' . $e->getMessage()); return array('data' => array(), 'total' => 0, 'total_in_database' => 0, 'showing_recent_only' => false); } } /** * Optimized fetch for large Pinecone databases * ADDED 2.6.1: Prevents crashes with large datasets * UPDATED 2.6.1: When searching, uses semantic search with embedded query for accurate results across all 13K+ records */ private function mxchat_fetch_pinecone_page_optimized($pinecone_options, $search_query = '', $page = 1, $per_page = 20, $bot_id = 'default', $content_type = '') { $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; try { // If user is searching, use semantic search with embedded query // This searches ALL records in Pinecone, not just fetched ones if (!empty($search_query)) { return $this->mxchat_semantic_search_pinecone($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type); } // For browsing (no search), use Pinecone's list endpoint for true pagination return $this->mxchat_list_pinecone_records($pinecone_options, $page, $per_page, $bot_id, $content_type); } catch (Exception $e) { //error_log('MxChat optimized fetch exception: ' . $e->getMessage()); MxChat_Admin::mxchat_log_debug('pinecone_error', 'Pinecone optimized fetch error: ' . $e->getMessage()); return array('data' => array(), 'total' => 0); } } /** * Semantic search across ALL Pinecone records using embedded search query * This allows users to find any of their 13K+ products by searching * ADDED 2.6.1 */ private function mxchat_semantic_search_pinecone($pinecone_options, $search_query, $page = 1, $per_page = 20, $bot_id = 'default', $content_type = '') { $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; // Get embedding for the search query $query_embedding = $this->mxchat_get_search_embedding($search_query); if (empty($query_embedding)) { // Fallback to text-based search if embedding fails return $this->mxchat_text_search_fallback($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type); } $query_url = "https://{$host}/query"; // Fetch more results to allow for filtering and pagination $fetch_limit = min(($page * $per_page) + 100, 500); $query_data = array( 'includeMetadata' => true, 'includeValues' => false, 'topK' => $fetch_limit, 'vector' => $query_embedding ); if (!empty($namespace)) { $query_data['namespace'] = $namespace; } // Add metadata filter for content type if specified if (!empty($content_type)) { $query_data['filter'] = array( 'type' => array('$eq' => $content_type) ); } $response = wp_remote_post($query_url, array( 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode($query_data), 'timeout' => 20 )); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { return $this->mxchat_text_search_fallback($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); $records = array(); if (isset($data['matches'])) { foreach ($data['matches'] as $match) { $metadata = $match['metadata'] ?? array(); $created_at = $metadata['created_at'] ?? $metadata['last_updated'] ?? $metadata['timestamp'] ?? time(); if (!is_numeric($created_at)) { $created_at = strtotime($created_at) ?: time(); } $records[] = (object) array( 'id' => $match['id'] ?? '', 'article_content' => $metadata['text'] ?? '', 'source_url' => $metadata['source_url'] ?? '', 'role_restriction' => $metadata['role_restriction'] ?? 'public', 'type' => $metadata['type'] ?? 'content', 'bot_id' => $bot_id, 'created_at' => $created_at, 'data_source' => 'pinecone', 'relevance_score' => $match['score'] ?? 0, 'chunk_index' => isset($metadata['chunk_index']) ? intval($metadata['chunk_index']) : null, 'total_chunks' => isset($metadata['total_chunks']) ? intval($metadata['total_chunks']) : null, 'is_chunked' => isset($metadata['is_chunked']) ? (bool) $metadata['is_chunked'] : false ); } } // For semantic search, results are already sorted by relevance (score) // No need to re-sort by date $total = count($records); $offset = ($page - 1) * $per_page; $paged_records = array_slice($records, $offset, $per_page); $this->mxchat_batch_fetch_role_restrictions($paged_records, $bot_id); return array( 'data' => $paged_records, 'total' => $total ); } /** * Get embedding vector for a search query * Uses the same embedding model configured for the knowledge base * ADDED 2.6.1 */ private function mxchat_get_search_embedding($search_query) { $options = get_option('mxchat_options', array()); $embedding_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; // Determine which API to use based on model if (strpos($embedding_model, 'voyage-') === 0) { return $this->mxchat_get_voyage_embedding($search_query, $options, $embedding_model); } elseif (strpos($embedding_model, 'gemini-') === 0) { return $this->mxchat_get_gemini_embedding($search_query, $options, $embedding_model); } else { return $this->mxchat_get_openai_embedding($search_query, $options, $embedding_model); } } /** * Get OpenAI embedding for search query */ private function mxchat_get_openai_embedding($text, $options, $model) { $api_key = $options['api_key'] ?? ''; if (empty($api_key)) { return null; } $response = wp_remote_post('https://api.openai.com/v1/embeddings', array( 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode(array( 'model' => $model, 'input' => $text )), 'timeout' => 15 )); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { return null; } $body = json_decode(wp_remote_retrieve_body($response), true); return $body['data'][0]['embedding'] ?? null; } /** * Get Voyage AI embedding for search query */ private function mxchat_get_voyage_embedding($text, $options, $model) { $api_key = $options['voyage_api_key'] ?? ''; if (empty($api_key)) { return null; } $request_body = array( 'model' => $model, 'input' => $text, 'input_type' => 'query' ); // Add output dimensions for voyage-3-large if configured if (strpos($model, 'voyage-3-large') === 0 && !empty($options['voyage_output_dimension'])) { $request_body['output_dimension'] = intval($options['voyage_output_dimension']); } $response = wp_remote_post('https://api.voyageai.com/v1/embeddings', array( 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode($request_body), 'timeout' => 15 )); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { return null; } $body = json_decode(wp_remote_retrieve_body($response), true); return $body['data'][0]['embedding'] ?? null; } /** * Get Google Gemini embedding for search query */ private function mxchat_get_gemini_embedding($text, $options, $model) { $api_key = $options['gemini_api_key'] ?? ''; if (empty($api_key)) { return null; } $url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:embedContent?key={$api_key}"; $request_body = array( 'model' => "models/{$model}", 'content' => array( 'parts' => array( array('text' => $text) ) ), 'taskType' => 'RETRIEVAL_QUERY' ); // Add output dimensions if configured if (!empty($options['gemini_output_dimension'])) { $request_body['outputDimensionality'] = intval($options['gemini_output_dimension']); } $response = wp_remote_post($url, array( 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => json_encode($request_body), 'timeout' => 15 )); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { return null; } $body = json_decode(wp_remote_retrieve_body($response), true); return $body['embedding']['values'] ?? null; } /** * Fallback text search when embedding fails * Fetches more records and filters by text match * ADDED 2.6.1 */ private function mxchat_text_search_fallback($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type) { $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; $query_url = "https://{$host}/query"; $query_vector = $this->mxchat_generate_optimized_query_vector(); // Fetch more records to search through $query_data = array( 'includeMetadata' => true, 'includeValues' => false, 'topK' => 1000, // Fetch more for text search 'vector' => $query_vector ); if (!empty($namespace)) { $query_data['namespace'] = $namespace; } $response = wp_remote_post($query_url, array( 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode($query_data), 'timeout' => 20 )); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { return array('data' => array(), 'total' => 0); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); $records = array(); $search_lower = strtolower($search_query); if (isset($data['matches'])) { foreach ($data['matches'] as $match) { $metadata = $match['metadata'] ?? array(); // Filter by content type if (!empty($content_type)) { $record_type = $metadata['type'] ?? 'content'; if ($record_type !== $content_type) { continue; } } // Text search filter $content = strtolower($metadata['text'] ?? ''); $source_url = strtolower($metadata['source_url'] ?? ''); if (strpos($content, $search_lower) === false && strpos($source_url, $search_lower) === false) { continue; } $created_at = $metadata['created_at'] ?? $metadata['last_updated'] ?? $metadata['timestamp'] ?? time(); if (!is_numeric($created_at)) { $created_at = strtotime($created_at) ?: time(); } $records[] = (object) array( 'id' => $match['id'] ?? '', 'article_content' => $metadata['text'] ?? '', 'source_url' => $metadata['source_url'] ?? '', 'role_restriction' => $metadata['role_restriction'] ?? 'public', 'type' => $metadata['type'] ?? 'content', 'bot_id' => $bot_id, 'created_at' => $created_at, 'data_source' => 'pinecone', 'chunk_index' => isset($metadata['chunk_index']) ? intval($metadata['chunk_index']) : null, 'total_chunks' => isset($metadata['total_chunks']) ? intval($metadata['total_chunks']) : null, 'is_chunked' => isset($metadata['is_chunked']) ? (bool) $metadata['is_chunked'] : false ); } } // Sort by date for text search results usort($records, function($a, $b) { return $b->created_at - $a->created_at; }); $total = count($records); $offset = ($page - 1) * $per_page; $paged_records = array_slice($records, $offset, $per_page); $this->mxchat_batch_fetch_role_restrictions($paged_records, $bot_id); return array( 'data' => $paged_records, 'total' => $total ); } /** * List Pinecone records using the list endpoint for true pagination (no search) * This allows browsing through all 13K+ records page by page * ADDED 2.6.1 */ private function mxchat_list_pinecone_records($pinecone_options, $page = 1, $per_page = 20, $bot_id = 'default', $content_type = '') { $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; // Pinecone's list endpoint returns vector IDs with pagination // We then fetch the metadata for those specific IDs $list_url = "https://{$host}/vectors/list"; // Calculate pagination token from page number // Pinecone uses cursor-based pagination, so we need to handle this differently $limit = $per_page * 2; // Fetch extra to account for filtering $list_params = array( 'limit' => $limit ); if (!empty($namespace)) { $list_params['namespace'] = $namespace; } // For pages beyond first, we need to use pagination_token // Store/retrieve pagination tokens in transients $pagination_key = 'mxchat_pinecone_page_' . md5($host . $namespace . $content_type); if ($page > 1) { $stored_tokens = get_transient($pagination_key); if ($stored_tokens && isset($stored_tokens[$page])) { $list_params['paginationToken'] = $stored_tokens[$page]; } } $response = wp_remote_post($list_url, array( 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode($list_params), 'timeout' => 15 )); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { // Fallback to query-based approach return $this->mxchat_query_based_list($pinecone_options, $page, $per_page, $bot_id, $content_type); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); // Store pagination token for next page if (!empty($data['pagination']['next'])) { $stored_tokens = get_transient($pagination_key) ?: array(); $stored_tokens[$page + 1] = $data['pagination']['next']; set_transient($pagination_key, $stored_tokens, 300); // 5 minute cache } $vector_ids = array(); if (isset($data['vectors'])) { foreach ($data['vectors'] as $vector) { $vector_ids[] = $vector['id']; } } // If list endpoint returned empty, fall back to query-based approach if (empty($vector_ids)) { return $this->mxchat_query_based_list($pinecone_options, $page, $per_page, $bot_id, $content_type); } // Fetch metadata for these vector IDs return $this->mxchat_fetch_vectors_by_ids_for_list($pinecone_options, $vector_ids, $page, $per_page, $bot_id, $content_type); } /** * Query-based listing fallback when list endpoint fails * ADDED 2.6.1 */ private function mxchat_query_based_list($pinecone_options, $page, $per_page, $bot_id, $content_type) { $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; $query_url = "https://{$host}/query"; $query_vector = $this->mxchat_generate_optimized_query_vector(); // Fetch records - use higher limit to cover large databases // Pinecone query API supports up to 10,000 topK // We fetch more than needed to get accurate total count and enable pagination $fetch_limit = 5000; $query_data = array( 'includeMetadata' => true, 'includeValues' => false, 'topK' => $fetch_limit, 'vector' => $query_vector ); if (!empty($namespace)) { $query_data['namespace'] = $namespace; } // Add content type filter if specified if (!empty($content_type)) { $query_data['filter'] = array( 'type' => array('$eq' => $content_type) ); } $response = wp_remote_post($query_url, array( 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode($query_data), 'timeout' => 30 )); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { return array('data' => array(), 'total' => 0); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); $records = array(); if (isset($data['matches'])) { foreach ($data['matches'] as $match) { $metadata = $match['metadata'] ?? array(); $created_at = $metadata['created_at'] ?? $metadata['last_updated'] ?? $metadata['timestamp'] ?? time(); if (!is_numeric($created_at)) { $created_at = strtotime($created_at) ?: time(); } $records[] = (object) array( 'id' => $match['id'] ?? '', 'article_content' => $metadata['text'] ?? '', 'source_url' => $metadata['source_url'] ?? '', 'role_restriction' => $metadata['role_restriction'] ?? 'public', 'type' => $metadata['type'] ?? 'content', 'bot_id' => $bot_id, 'created_at' => $created_at, 'data_source' => 'pinecone', 'chunk_index' => isset($metadata['chunk_index']) ? intval($metadata['chunk_index']) : null, 'total_chunks' => isset($metadata['total_chunks']) ? intval($metadata['total_chunks']) : null, 'is_chunked' => isset($metadata['is_chunked']) ? (bool) $metadata['is_chunked'] : false ); } } // Sort by created_at (newest first) usort($records, function($a, $b) { return $b->created_at - $a->created_at; }); $total = count($records); $offset = ($page - 1) * $per_page; $paged_records = array_slice($records, $offset, $per_page); $this->mxchat_batch_fetch_role_restrictions($paged_records, $bot_id); return array( 'data' => $paged_records, 'total' => $total ); } /** * Fetch specific vectors by their IDs and format for display * ADDED 2.6.1 */ private function mxchat_fetch_vectors_by_ids_for_list($pinecone_options, $vector_ids, $page, $per_page, $bot_id, $content_type) { $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; $fetch_url = "https://{$host}/vectors/fetch"; $fetch_data = array( 'ids' => $vector_ids ); if (!empty($namespace)) { $fetch_data['namespace'] = $namespace; } $response = wp_remote_post($fetch_url, array( 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode($fetch_data), 'timeout' => 15 )); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { return array('data' => array(), 'total' => 0); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); $records = array(); if (isset($data['vectors'])) { foreach ($data['vectors'] as $vector_id => $vector_data) { $metadata = $vector_data['metadata'] ?? array(); // Filter by content type if specified if (!empty($content_type)) { $record_type = $metadata['type'] ?? 'content'; if ($record_type !== $content_type) { continue; } } $created_at = $metadata['created_at'] ?? $metadata['last_updated'] ?? $metadata['timestamp'] ?? time(); if (!is_numeric($created_at)) { $created_at = strtotime($created_at) ?: time(); } $records[] = (object) array( 'id' => $vector_id, 'article_content' => $metadata['text'] ?? '', 'source_url' => $metadata['source_url'] ?? '', 'role_restriction' => $metadata['role_restriction'] ?? 'public', 'type' => $metadata['type'] ?? 'content', 'bot_id' => $bot_id, 'created_at' => $created_at, 'data_source' => 'pinecone', 'chunk_index' => isset($metadata['chunk_index']) ? intval($metadata['chunk_index']) : null, 'total_chunks' => isset($metadata['total_chunks']) ? intval($metadata['total_chunks']) : null, 'is_chunked' => isset($metadata['is_chunked']) ? (bool) $metadata['is_chunked'] : false ); } } // Sort by date usort($records, function($a, $b) { return $b->created_at - $a->created_at; }); $total = count($records); $paged_records = array_slice($records, 0, $per_page); $this->mxchat_batch_fetch_role_restrictions($paged_records, $bot_id); return array( 'data' => $paged_records, 'total' => $total ); } /** * Generate a single optimized query vector for fetching records * Uses a center-weighted approach for best coverage */ private function mxchat_generate_optimized_query_vector() { $dimensions = $this->mxchat_get_embedding_dimensions(); $vector = array(); // Create a normalized center-weighted vector $center = $dimensions / 2; for ($i = 0; $i < $dimensions; $i++) { $distance = abs($i - $center) / $center; $vector[] = (1 - $distance) * 0.5; } // Normalize to unit length $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $vector))); if ($magnitude > 0) { $vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $vector); } return $vector; } /** * Batch fetch role restrictions for a set of records * Uses a single query instead of N queries * ADDED 2.6.1: Prevents N+1 query problem */ private function mxchat_batch_fetch_role_restrictions(&$records, $bot_id = 'default') { if (empty($records)) { return; } global $wpdb; $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles'; // Check if table exists $table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$roles_table}'"); if (!$table_exists) { return; } // Get all vector IDs that need role lookup $vector_ids = array(); foreach ($records as $record) { if (empty($record->role_restriction) || $record->role_restriction === 'public') { $vector_ids[] = $record->id; } } if (empty($vector_ids)) { return; } // Check if bot_id column exists $columns = $wpdb->get_col("SHOW COLUMNS FROM {$roles_table}"); $has_bot_id = in_array('bot_id', $columns); // Build single query with IN clause $placeholders = implode(',', array_fill(0, count($vector_ids), '%s')); if ($has_bot_id) { $query = $wpdb->prepare( "SELECT vector_id, role_restriction FROM {$roles_table} WHERE vector_id IN ({$placeholders}) AND bot_id = %s", array_merge($vector_ids, array($bot_id)) ); } else { $query = $wpdb->prepare( "SELECT vector_id, role_restriction FROM {$roles_table} WHERE vector_id IN ({$placeholders})", $vector_ids ); } $results = $wpdb->get_results($query, OBJECT_K); // Apply role restrictions to records foreach ($records as &$record) { if (isset($results[$record->id])) { $record->role_restriction = $results[$record->id]->role_restriction; } } } /** * Safe version of get_recent_entries with memory limits * ADDED 2.6.1: Prevents memory exhaustion */ private function mxchat_get_recent_entries_safe($pinecone_options, $bot_id = 'default', $limit = 500) { global $wpdb; $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; if (empty($api_key) || empty($host)) { return array(); } try { $all_records = array(); $seen_ids = array(); $query_url = "https://{$host}/query"; // Use only 2 query vectors instead of 5 for better performance $fixed_vectors = array_slice($this->mxchat_generate_fixed_query_vectors(), 0, 2); foreach ($fixed_vectors as $query_vector) { // Limit topK to prevent memory issues $topK = min(500, $limit); $query_data = array( 'includeMetadata' => true, 'includeValues' => false, 'topK' => $topK, 'vector' => $query_vector ); if (!empty($namespace)) { $query_data['namespace'] = $namespace; } $response = wp_remote_post($query_url, array( 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode($query_data), 'timeout' => 15 )); if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (isset($data['matches'])) { foreach ($data['matches'] as $match) { $match_id = $match['id'] ?? ''; if (!empty($match_id) && !isset($seen_ids[$match_id])) { $metadata = $match['metadata'] ?? array(); $created_at = $metadata['created_at'] ?? $metadata['last_updated'] ?? $metadata['timestamp'] ?? time(); if (!is_numeric($created_at)) { $created_at = strtotime($created_at) ?: time(); } $all_records[] = (object) array( 'id' => $match_id, 'article_content' => $metadata['text'] ?? '', 'source_url' => $metadata['source_url'] ?? '', 'role_restriction' => $metadata['role_restriction'] ?? 'public', 'type' => $metadata['type'] ?? 'content', 'bot_id' => $bot_id, 'created_at' => $created_at, 'data_source' => 'pinecone', 'chunk_index' => isset($metadata['chunk_index']) ? intval($metadata['chunk_index']) : null, 'total_chunks' => isset($metadata['total_chunks']) ? intval($metadata['total_chunks']) : null, 'is_chunked' => isset($metadata['is_chunked']) ? (bool) $metadata['is_chunked'] : false ); $seen_ids[$match_id] = true; // Stop if we've reached our limit if (count($all_records) >= $limit) { break 2; } } } } } // Minimal delay between requests usleep(50000); // 0.05 second delay } // Sort by created_at (newest first) and apply limit usort($all_records, function($a, $b) { return $b->created_at - $a->created_at; }); $limited_records = array_slice($all_records, 0, $limit); // Batch fetch role restrictions $this->mxchat_batch_fetch_role_restrictions($limited_records, $bot_id); return $limited_records; } catch (Exception $e) { //error_log('MxChat safe fetch exception: ' . $e->getMessage()); MxChat_Admin::mxchat_log_debug('pinecone_error', 'Pinecone safe fetch error: ' . $e->getMessage()); return array(); } } /** * Get embedding dimensions based on the selected model * ADD THIS NEW FUNCTION */ private function mxchat_get_embedding_dimensions() { $options = get_option('mxchat_options', array()); $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; // Define dimensions for different models $model_dimensions = array( 'text-embedding-ada-002' => 1536, 'text-embedding-3-small' => 1536, 'text-embedding-3-large' => 3072, 'voyage-2' => 1024, 'voyage-large-2' => 1536, 'voyage-3-large' => 2048, 'gemini-embedding-001' => 1536, ); // Check if it's a voyage model with custom dimensions if (strpos($selected_model, 'voyage-3-large') === 0) { $custom_dimensions = $options['voyage_output_dimension'] ?? 2048; return intval($custom_dimensions); } // Check if it's a gemini model with custom dimensions if (strpos($selected_model, 'gemini-embedding') === 0) { $custom_dimensions = $options['gemini_output_dimension'] ?? 1536; return intval($custom_dimensions); } // Return known dimensions or default to 1536 return $model_dimensions[$selected_model] ?? 1536; } /** * Generate random unit vector with correct dimensions * ADD THIS NEW FUNCTION */ private function mxchat_generate_random_vector() { $dimensions = $this->mxchat_get_embedding_dimensions(); $random_vector = array(); for ($i = 0; $i < $dimensions; $i++) { $random_vector[] = (rand(-1000, 1000) / 1000.0); } // Normalize the vector to unit length $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector))); if ($magnitude > 0) { $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector); } return $random_vector; } /** * Get recent entries from Pinecone * UPDATED 2.6.1: Now uses safe version with memory limits to prevent crashes * @deprecated Use mxchat_get_recent_entries_safe() instead for new code */ private function mxchat_get_recent_1k_entries($pinecone_options, $bot_id = 'default') { // Delegate to the safe version with a reasonable limit // This prevents crashes with large datasets (13K+ products) return $this->mxchat_get_recent_entries_safe($pinecone_options, $bot_id, 500); } /** * Generate fixed query vectors for consistent results */ private function mxchat_generate_fixed_query_vectors() { $dimensions = $this->mxchat_get_embedding_dimensions(); $vectors = array(); // Create 5 fixed vectors with different patterns for better coverage $patterns = array( 'zeros_with_ones' => 0.1, // Mostly zeros with some 1s 'ascending' => 0.2, // Ascending pattern 'descending' => 0.3, // Descending pattern 'alternating' => 0.4, // Alternating positive/negative 'center_weighted' => 0.5 // Higher values in center ); foreach ($patterns as $pattern_name => $seed) { $vector = array(); for ($i = 0; $i < $dimensions; $i++) { switch ($pattern_name) { case 'zeros_with_ones': $vector[] = ($i % 10 === 0) ? 1.0 : 0.0; break; case 'ascending': $vector[] = ($i / $dimensions) * 2 - 1; // Range -1 to 1 break; case 'descending': $vector[] = (($dimensions - $i) / $dimensions) * 2 - 1; break; case 'alternating': $vector[] = ($i % 2 === 0) ? $seed : -$seed; break; case 'center_weighted': $center = $dimensions / 2; $distance = abs($i - $center) / $center; $vector[] = (1 - $distance) * $seed; break; } } // Normalize the vector to unit length $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $vector))); if ($magnitude > 0) { $vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $vector); } $vectors[] = $vector; } return $vectors; } /** * Scan Pinecone for processed content * UPDATED 2.6.2: Uses direct ID lookup via fetch API instead of random vector scanning * This removes the 10K record limit and scales to any database size * * @param array $pinecone_options Pinecone configuration options * @param array $post_ids Optional array of specific post IDs to check (if empty, checks all published posts) * @return array Processed data keyed by post ID */ public function mxchat_scan_pinecone_for_processed_content($pinecone_options, $post_ids = array()) { $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; if (empty($api_key) || empty($host)) { return array(); } try { // If no specific post IDs provided, get all published posts if (empty($post_ids)) { $posts = get_posts(array( 'post_type' => 'any', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, )); $post_ids = $posts; } if (empty($post_ids)) { return array(); } // Build a map of vector_id => post data for lookup $vector_id_map = array(); foreach ($post_ids as $post_id) { $permalink = get_permalink($post_id); if ($permalink) { $vector_id = md5($permalink); $vector_id_map[$vector_id] = array( 'post_id' => $post_id, 'url' => $permalink ); } } if (empty($vector_id_map)) { return array(); } // Batch check Pinecone using fetch API (max 1000 IDs per request) $all_vector_ids = array_keys($vector_id_map); $chunks = array_chunk($all_vector_ids, 1000); $processed_data = array(); foreach ($chunks as $chunk) { $fetch_url = "https://{$host}/vectors/fetch"; $response = wp_remote_post($fetch_url, array( 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode(array('ids' => $chunk)), 'timeout' => 30 )); if (is_wp_error($response)) { continue; } $response_code = wp_remote_retrieve_response_code($response); if ($response_code !== 200) { continue; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); // Process returned vectors if (isset($data['vectors']) && is_array($data['vectors'])) { foreach ($data['vectors'] as $vector_id => $vector_data) { if (isset($vector_id_map[$vector_id])) { $post_info = $vector_id_map[$vector_id]; $post_id = $post_info['post_id']; $metadata = $vector_data['metadata'] ?? array(); $created_at = $metadata['created_at'] ?? ''; $processed_date = 'Recently'; $timestamp = current_time('timestamp'); if (!empty($created_at)) { $ts = is_numeric($created_at) ? $created_at : strtotime($created_at); if ($ts) { $timestamp = $ts; $processed_date = human_time_diff($ts, current_time('timestamp')) . ' ago'; } } $processed_data[$post_id] = array( 'db_id' => $vector_id, 'processed_date' => $processed_date, 'url' => $post_info['url'], 'source' => 'pinecone', 'timestamp' => $timestamp ); } } } } return $processed_data; } catch (Exception $e) { return array(); } } /** * Get total count from Pinecone stats API * UPDATED: Removed cache fallback reference */ private function mxchat_get_pinecone_total_count($pinecone_options, $bot_id = 'default') { $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; if (empty($api_key) || empty($host)) { return 0; } try { $stats_url = "https://{$host}/describe_index_stats"; // describe_index_stats doesn't need a body, just the POST request $response = wp_remote_post($stats_url, array( 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), 'body' => '{}', 'timeout' => 15 )); if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { $body = wp_remote_retrieve_body($response); $stats_data = json_decode($body, true); // If namespace is specified, get count from that specific namespace // Pinecone stats response format: { namespaces: { "ns": { vectorCount: N } }, totalVectorCount: N } if (!empty($namespace) && isset($stats_data['namespaces'][$namespace]['vectorCount'])) { $namespace_count = intval($stats_data['namespaces'][$namespace]['vectorCount']); //error_log('DEBUG: Got namespace-specific count: ' . $namespace_count . ' for namespace: ' . $namespace); return $namespace_count; } // If no namespace specified or namespace not found in response, use total $total_count = $stats_data['totalVectorCount'] ?? 0; if ($total_count > 0) { //error_log('DEBUG: Got total count from stats API: ' . $total_count); return intval($total_count); } } // If stats API fails, return 0 instead of using cache return 0; } catch (Exception $e) { //error_log('DEBUG: Exception getting total count: ' . $e->getMessage()); return 0; } } /** * Get bot-specific Pinecone configuration for database operations */ public function mxchat_get_bot_pinecone_options($bot_id = 'default') { //error_log('DEBUG: Getting Pinecone options for bot: ' . $bot_id); // If default bot or multi-bot add-on not active, use default Pinecone config if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) { $addon_options = get_option('mxchat_pinecone_addon_options', array()); //error_log('DEBUG: Using default Pinecone options'); return $addon_options; } // Get bot-specific configuration using the filter $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id); //error_log('DEBUG: Bot config from filter: ' . print_r($bot_config, true)); // Check if we got valid bot-specific config if (!empty($bot_config) && isset($bot_config['use_pinecone']) && $bot_config['use_pinecone']) { // Convert bot config to the format expected by fetch functions $pinecone_options = array( 'mxchat_use_pinecone' => '1', 'mxchat_pinecone_api_key' => $bot_config['api_key'] ?? '', 'mxchat_pinecone_host' => $bot_config['host'] ?? '', 'mxchat_pinecone_namespace' => $bot_config['namespace'] ?? '', 'mxchat_pinecone_environment' => '', 'mxchat_pinecone_index' => '' ); //error_log('DEBUG: Returning bot-specific Pinecone options for bot: ' . $bot_id); return $pinecone_options; } // Fallback to default options if bot-specific config is invalid //error_log('DEBUG: Bot-specific config invalid, falling back to default'); return get_option('mxchat_pinecone_addon_options', array()); } /** * Get bot-specific Pinecone configuration * Used in the knowledge retrieval functions */ private function get_bot_pinecone_config($bot_id = 'default') { //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id); // If default bot or multi-bot add-on not active, use default Pinecone config if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) { //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')"); $addon_options = get_option('mxchat_pinecone_addon_options', array()); $config = array( 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'), 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '', 'host' => $addon_options['mxchat_pinecone_host'] ?? '', 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? '' ); //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false')); return $config; } //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id); // Hook for multi-bot add-on to provide bot-specific Pinecone config $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id); if (!empty($bot_pinecone_config)) { //error_log("MXCHAT DEBUG: Got bot-specific config from filter"); //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set')); //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set')); //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set')); } else { //error_log("MXCHAT DEBUG: Filter returned empty config!"); } return is_array($bot_pinecone_config) ? $bot_pinecone_config : array(); } /** * Fetches vectors from Pinecone using provided IDs (for content selection feature) */ public function fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) { $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; if (empty($api_key) || empty($host) || empty($vector_ids)) { return array(); } try { $fetch_url = "https://{$host}/vectors/fetch"; // Pinecone fetch API allows fetching specific vectors by ID $fetch_data = array( 'ids' => array_values($vector_ids) ); $response = wp_remote_post($fetch_url, array( 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode($fetch_data), 'timeout' => 30 )); if (is_wp_error($response)) { return array(); } $response_code = wp_remote_retrieve_response_code($response); if ($response_code !== 200) { return array(); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (!isset($data['vectors'])) { return array(); } $processed_data = array(); foreach ($data['vectors'] as $vector_id => $vector_data) { $metadata = $vector_data['metadata'] ?? array(); $source_url = $metadata['source_url'] ?? ''; if (!empty($source_url)) { $post_id = url_to_postid($source_url); if ($post_id) { $created_at = $metadata['created_at'] ?? ''; $processed_date = 'Recently'; // Default if (!empty($created_at)) { $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at); if ($timestamp) { $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago'; } } $processed_data[$post_id] = array( 'db_id' => $vector_id, 'processed_date' => $processed_date, 'url' => $source_url, 'source' => 'pinecone', 'timestamp' => $timestamp ?? current_time('timestamp') ); } } } return $processed_data; } catch (Exception $e) { return array(); } } // ======================================== // PINECONE DELETE OPERATIONS // ======================================== /** * Delete all vectors from Pinecone * Loops until all vectors are deleted (handles large databases) */ public function mxchat_delete_all_from_pinecone($pinecone_options, $content_type_filter = '') { $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; if (empty($api_key) || empty($host)) { return array( 'success' => false, 'message' => 'Missing Pinecone API credentials' ); } try { $total_deleted = 0; $failed_batches = 0; $max_iterations = 100; // Safety limit to prevent infinite loops $iteration = 0; // Loop until no more vectors are found do { $iteration++; // Get a batch of vector IDs from Pinecone $records = $this->mxchat_get_recent_1k_entries($pinecone_options); $vector_ids = array(); foreach ($records as $record) { if (!empty($record->id)) { // If content type filter is active, only include matching records if (!empty($content_type_filter)) { $record_type = isset($record->type) ? $record->type : ''; if ($record_type !== $content_type_filter) { continue; } } $vector_ids[] = $record->id; } } // If no matching vectors found, we're done // (either no records at all, or all remaining records are non-matching types) if (empty($vector_ids)) { break; } // Delete vectors in batches (Pinecone has limits on batch operations) $batch_size = 100; $batches = array_chunk($vector_ids, $batch_size); foreach ($batches as $batch) { $result = $this->mxchat_delete_pinecone_batch($batch, $api_key, $host); if ($result['success']) { $total_deleted += count($batch); } else { $failed_batches++; } } // Small delay to avoid rate limiting usleep(100000); // 100ms } while ($iteration < $max_iterations); if ($total_deleted === 0) { return array( 'success' => true, 'message' => 'No vectors found to delete' ); } if ($failed_batches > 0) { return array( 'success' => false, 'message' => sprintf('Deleted %d vectors, but %d batches failed', $total_deleted, $failed_batches) ); } return array( 'success' => true, 'message' => "Successfully deleted {$total_deleted} vectors from Pinecone" ); } catch (Exception $e) { return array( 'success' => false, 'message' => $e->getMessage() ); } } /** * Deletes batch of vectors from Pinecone database */ public function mxchat_delete_pinecone_batch($vector_ids, $api_key, $host) { // Build the API endpoint $api_endpoint = "https://{$host}/vectors/delete"; // Prepare the request body with the IDs $request_body = array( 'ids' => $vector_ids ); // Make the deletion request $response = wp_remote_post($api_endpoint, array( 'headers' => array( 'Api-Key' => $api_key, 'accept' => 'application/json', 'content-type' => 'application/json' ), 'body' => wp_json_encode($request_body), 'timeout' => 60, // Increased timeout for batch operations 'method' => 'POST' )); // Handle WordPress HTTP API errors if (is_wp_error($response)) { MxChat_Admin::mxchat_log_debug('pinecone_error', 'Pinecone batch deletion failed: ' . $response->get_error_message()); return array( 'success' => false, 'message' => $response->get_error_message() ); } // Check response status $response_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); // Pinecone returns 200 for successful deletion if ($response_code !== 200) { MxChat_Admin::mxchat_log_debug('pinecone_error', 'Pinecone batch deletion failed (HTTP ' . $response_code . ')', array('response' => substr($response_body, 0, 200))); return array( 'success' => false, 'message' => sprintf( 'Pinecone API error (HTTP %d): %s', $response_code, $response_body ) ); } return array( 'success' => true, 'message' => 'Batch deleted successfully from Pinecone' ); } /** * Deletes vector from Pinecone using API request */ public function mxchat_delete_from_pinecone_by_vector_id($vector_id, $api_key, $host, $namespace = '') { //error_log('=== PINECONE DELETE OPERATION ==='); //error_log('Vector ID: ' . $vector_id); //error_log('Host: ' . $host); //error_log('API Key: ' . (empty($api_key) ? 'EMPTY' : 'SET')); // First, let's verify the vector exists before trying to delete $fetch_url = "https://{$host}/vectors/fetch"; $fetch_params = array( 'ids' => array($vector_id) ); // Add namespace if provided (though you said you're not using namespaces) if (!empty($namespace)) { $fetch_params['namespace'] = $namespace; } // Construct URL with query parameters for GET request $fetch_url_with_params = $fetch_url . '?' . http_build_query($fetch_params); $fetch_response = wp_remote_get($fetch_url_with_params, array( 'headers' => array( 'Api-Key' => $api_key, 'accept' => 'application/json' ), 'timeout' => 15 )); if (!is_wp_error($fetch_response) && wp_remote_retrieve_response_code($fetch_response) === 200) { $fetch_body = wp_remote_retrieve_body($fetch_response); $fetch_data = json_decode($fetch_body, true); //error_log('DEBUG: Fetch response: ' . print_r($fetch_data, true)); if (isset($fetch_data['vectors']) && isset($fetch_data['vectors'][$vector_id])) { //error_log('DEBUG: Vector EXISTS in this index before deletion'); } else { //error_log('WARNING: Vector NOT FOUND in this index! It may be in a different bot\'s index'); // You might want to return an error here } } else { //error_log('DEBUG: Could not fetch vector to verify existence'); } // Now proceed with deletion $api_endpoint = "https://{$host}/vectors/delete"; // Prepare the request body with the ID $request_body = array( 'ids' => array($vector_id) ); // Add namespace if provided if (!empty($namespace)) { $request_body['namespace'] = $namespace; } //error_log('DEBUG: Delete request body: ' . json_encode($request_body)); //error_log('DEBUG: Delete endpoint: ' . $api_endpoint); // Make the deletion request $response = wp_remote_post($api_endpoint, array( 'headers' => array( 'Api-Key' => $api_key, 'accept' => 'application/json', 'content-type' => 'application/json' ), 'body' => wp_json_encode($request_body), 'timeout' => 30 )); // Handle WordPress HTTP API errors if (is_wp_error($response)) { //error_log('DEBUG: WP Error: ' . $response->get_error_message()); return array( 'success' => false, 'message' => $response->get_error_message() ); } // Check response status $response_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); //error_log('DEBUG: Delete response code: ' . $response_code); //error_log('DEBUG: Delete response body: ' . $response_body); // Pinecone returns 200 for successful deletion (even if vector didn't exist) if ($response_code !== 200) { //error_log('DEBUG: Non-200 response from Pinecone'); return array( 'success' => false, 'message' => sprintf( 'Pinecone API error (HTTP %d): %s', $response_code, $response_body ) ); } // After deletion, verify it's actually gone sleep(1); // Give Pinecone a moment to process $verify_response = wp_remote_get($fetch_url_with_params, array( 'headers' => array( 'Api-Key' => $api_key, 'accept' => 'application/json' ), 'timeout' => 15 )); if (!is_wp_error($verify_response) && wp_remote_retrieve_response_code($verify_response) === 200) { $verify_body = wp_remote_retrieve_body($verify_response); $verify_data = json_decode($verify_body, true); if (isset($verify_data['vectors']) && isset($verify_data['vectors'][$vector_id])) { //error_log('ERROR: Vector STILL EXISTS after deletion attempt!'); return array( 'success' => false, 'message' => 'Vector still exists after deletion attempt' ); } else { //error_log('SUCCESS: Vector confirmed deleted (or never existed)'); } } //error_log('=== END PINECONE DELETE OPERATION ==='); return array( 'success' => true, 'message' => 'Vector deleted successfully from Pinecone' ); } /** * Deletes data from Pinecone index using API key */ private function mxchat_delete_from_pinecone($urls, $api_key, $environment, $index_name) { // Get the Pinecone host from options (matching your store_in_pinecone_main pattern) $options = get_option('mxchat_pinecone_addon_options'); $host = $options['mxchat_pinecone_host'] ?? ''; if (empty($host)) { return array( 'success' => false, 'message' => 'Pinecone host is not configured. Please set the host in your settings.' ); } // Build API endpoint using the configured host $api_endpoint = "https://{$host}/vectors/delete"; // Create vector IDs from URLs (matching your store method's ID generation) $vector_ids = array_map('md5', $urls); // Prepare the delete request body $request_body = array( 'ids' => $vector_ids, 'filter' => array( 'source_url' => array( '$in' => $urls ) ) ); // Make the deletion request $response = wp_remote_post($api_endpoint, array( 'headers' => array( 'Api-Key' => $api_key, 'accept' => 'application/json', 'content-type' => 'application/json' ), 'body' => wp_json_encode($request_body), 'timeout' => 30, 'data_format' => 'body' )); // Handle WordPress HTTP API errors if (is_wp_error($response)) { return array( 'success' => false, 'message' => $response->get_error_message() ); } // Check response status $response_code = wp_remote_retrieve_response_code($response); if ($response_code !== 200) { $body = wp_remote_retrieve_body($response); return array( 'success' => false, 'message' => sprintf( 'Pinecone API error (HTTP %d): %s', $response_code, $body ) ); } // Parse response body $body = wp_remote_retrieve_body($response); $response_data = json_decode($body, true); // Final validation of the response if (json_last_error() !== JSON_ERROR_NONE) { return array( 'success' => false, 'message' => 'Failed to parse Pinecone response: ' . json_last_error_msg() ); } return array( 'success' => true, 'message' => sprintf('Successfully deleted %d vectors from Pinecone', count($vector_ids)) ); } /** * Retrieves processed content from Pinecone API * * @param array $pinecone_options Pinecone configuration options * @param array $post_ids Optional array of specific post IDs to check (if empty, checks all) * @return array Processed data keyed by post ID */ public function mxchat_get_pinecone_processed_content($pinecone_options, $post_ids = array()) { $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; if (empty($api_key) || empty($host)) { return array(); } $pinecone_data = array(); try { // Always get fresh data from Pinecone $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options, $post_ids); // Method 2: Final fallback - try stats endpoint (if available) if (empty($pinecone_data)) { $stats_url = "https://{$host}/describe_index_stats"; $response = wp_remote_post($stats_url, array( 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode(array()), 'timeout' => 30 )); if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { $body = wp_remote_retrieve_body($response); $stats_data = json_decode($body, true); } } } catch (Exception $e) { // Log error but don't return cached data } return $pinecone_data; } // ======================================== // HELPER METHODS // ======================================== /** * Validates Pinecone API credentials */ private function mxchat_validate_pinecone_credentials($api_key, $host) { if (empty($api_key) || empty($host)) { return false; } return true; } /** * Get Pinecone API credentials from options */ private function mxchat_get_pinecone_credentials() { $options = get_option('mxchat_options', array()); return array( 'api_key' => isset($options['pinecone_api_key']) ? $options['pinecone_api_key'] : '', 'host' => isset($options['pinecone_host']) ? $options['pinecone_host'] : '' ); } /** * Log Pinecone operation errors */ private function log_pinecone_error($operation, $error_message) { //error_log("MxChat Pinecone {$operation} Error: " . $error_message); } // ======================================== // STATIC ACCESS METHODS (for backward compatibility) // ======================================== /** * Get singleton instance */ public static function get_instance() { static $instance = null; if ($instance === null) { $instance = new self(); } return $instance; } } // Initialize the Pinecone manager $mxchat_pinecone_manager = MxChat_Pinecone_Manager::get_instance();