PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.4.0
MxChat – AI Chatbot & Content Generation for WordPress v2.4.0
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / admin / class-pinecone-manager.php

class-pinecone-manager.php in MxChat – AI Chatbot & Content Generation for WordPress 2.4.0, at admin/class-pinecone-manager.php

1,125 lines 41.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * File: admin/class-pinecone-manager.php
4 *
5 * Handles all Pinecone vector database operations for MxChat
6 */
7
8 if (!defined('ABSPATH')) {
9 exit; // Exit if accessed directly
10 }
11
12 class MxChat_Pinecone_Manager {
13
14 /**
15 * Constructor
16 */
17 public function __construct() {
18 // Hook into WordPress actions if needed
19 add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1);
20 }
21
22 // ========================================
23 // PINECONE FETCH OPERATIONS
24 // ========================================
25
26
27 /**
28 * Fetches 1K most recent records from Pinecone
29 */
30 public function mxchat_fetch_pinecone_records($pinecone_options, $search_query = '', $page = 1, $per_page = 20) {
31 //error_log('=== DEBUG: mxchat_fetch_pinecone_records started (improved) ===');
32
33 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
34 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
35
36 if (empty($api_key) || empty($host)) {
37 //error_log('DEBUG: Missing required Pinecone parameters');
38 return array('data' => array(), 'total' => 0, 'total_in_database' => 0, 'showing_recent_only' => false);
39 }
40
41 try {
42 // Get total count for the banner message
43 $total_in_database = $this->mxchat_get_pinecone_total_count($pinecone_options);
44
45 // Check cache first for consistency
46 $cache_key = 'mxchat_pinecone_recent_1k_cache';
47 $all_records = get_transient($cache_key);
48
49 if ($all_records === false) {
50 // Cache miss - get fresh data
51 $all_records = $this->mxchat_get_recent_1k_entries($pinecone_options);
52 }
53
54 // Filter by search query if provided
55 if (!empty($search_query)) {
56 $all_records = array_filter($all_records, function($record) use ($search_query) {
57 $content = $record->article_content ?? '';
58 $source_url = $record->source_url ?? '';
59 return stripos($content, $search_query) !== false || stripos($source_url, $search_query) !== false;
60 });
61 }
62
63 // Handle pagination
64 $total = count($all_records);
65 $offset = ($page - 1) * $per_page;
66 $paged_records = array_slice($all_records, $offset, $per_page);
67
68 //error_log('DEBUG: Returning ' . count($paged_records) . ' records (page ' . $page . ' of ' . ceil($total / $per_page) . ')');
69
70 return array(
71 'data' => $paged_records,
72 'total' => $total,
73 'total_in_database' => $total_in_database,
74 'showing_recent_only' => ($total_in_database > 1000)
75 );
76
77 } catch (Exception $e) {
78 //error_log('DEBUG: Exception: ' . $e->getMessage());
79 return array('data' => array(), 'total' => 0, 'total_in_database' => 0, 'showing_recent_only' => false);
80 }
81 }
82
83
84 /**
85 * Get embedding dimensions based on the selected model
86 * ADD THIS NEW FUNCTION
87 */
88 private function mxchat_get_embedding_dimensions() {
89 $options = get_option('mxchat_options', array());
90 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
91
92 // Define dimensions for different models
93 $model_dimensions = array(
94 'text-embedding-ada-002' => 1536,
95 'text-embedding-3-small' => 1536,
96 'text-embedding-3-large' => 3072,
97 'voyage-2' => 1024,
98 'voyage-large-2' => 1536,
99 'voyage-3-large' => 2048,
100 'gemini-embedding-001' => 1536,
101 );
102
103 // Check if it's a voyage model with custom dimensions
104 if (strpos($selected_model, 'voyage-3-large') === 0) {
105 $custom_dimensions = $options['voyage_output_dimension'] ?? 2048;
106 return intval($custom_dimensions);
107 }
108
109 // Check if it's a gemini model with custom dimensions
110 if (strpos($selected_model, 'gemini-embedding') === 0) {
111 $custom_dimensions = $options['gemini_output_dimension'] ?? 1536;
112 return intval($custom_dimensions);
113 }
114
115 // Return known dimensions or default to 1536
116 return $model_dimensions[$selected_model] ?? 1536;
117 }
118
119 /**
120 * Generate random unit vector with correct dimensions
121 * ADD THIS NEW FUNCTION
122 */
123 private function mxchat_generate_random_vector() {
124 $dimensions = $this->mxchat_get_embedding_dimensions();
125
126 $random_vector = array();
127 for ($i = 0; $i < $dimensions; $i++) {
128 $random_vector[] = (rand(-1000, 1000) / 1000.0);
129 }
130
131 // Normalize the vector to unit length
132 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
133 if ($magnitude > 0) {
134 $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
135 }
136
137 return $random_vector;
138 }
139
140
141 /**
142 * Get 1,000 most recent entries from Pinecone (UPDATED - Include role_restriction)
143 */
144 private function mxchat_get_recent_1k_entries($pinecone_options) {
145 global $wpdb;
146 //error_log('=== DEBUG: mxchat_get_recent_1k_entries started (fixed version) ===');
147
148 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
149 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
150
151 if (empty($api_key) || empty($host)) {
152 return array();
153 }
154
155 try {
156 // First, try to get all vectors using a comprehensive scan approach
157 $all_records = array();
158 $seen_ids = array();
159 $query_url = "https://{$host}/query";
160
161 // Use a more systematic approach - try to get diverse samples that cover more of the database
162 $fixed_vectors = $this->mxchat_generate_fixed_query_vectors();
163
164 foreach ($fixed_vectors as $vector_index => $query_vector) {
165 //error_log('DEBUG: Using fixed query vector ' . ($vector_index + 1) . '/' . count($fixed_vectors));
166
167 $query_data = array(
168 'includeMetadata' => true,
169 'includeValues' => false,
170 'topK' => 3000, // Get more per query
171 'vector' => $query_vector
172 );
173
174 $response = wp_remote_post($query_url, array(
175 'headers' => array(
176 'Api-Key' => $api_key,
177 'Content-Type' => 'application/json'
178 ),
179 'body' => json_encode($query_data),
180 'timeout' => 30
181 ));
182
183 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
184 $body = wp_remote_retrieve_body($response);
185 $data = json_decode($body, true);
186
187 if (isset($data['matches'])) {
188 foreach ($data['matches'] as $match) {
189 $match_id = $match['id'] ?? '';
190 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
191 $metadata = $match['metadata'] ?? array();
192
193 // Get created_at timestamp - try multiple possible fields
194 $created_at = $metadata['created_at'] ??
195 $metadata['last_updated'] ??
196 $metadata['timestamp'] ??
197 time();
198
199 // Ensure valid timestamp
200 if (!is_numeric($created_at)) {
201 $created_at = strtotime($created_at) ?: time();
202 }
203
204 $all_records[] = (object) array(
205 'id' => $match_id,
206 'article_content' => $metadata['text'] ?? '',
207 'source_url' => $metadata['source_url'] ?? '',
208 'role_restriction' => $metadata['role_restriction'] ?? 'public', // NEW: Include from Pinecone metadata
209 'created_at' => $created_at,
210 'data_source' => 'pinecone'
211 );
212
213 $seen_ids[$match_id] = true;
214 }
215 }
216 }
217 }
218
219 // Small delay between requests
220 usleep(200000); // 0.2 second delay
221 }
222
223 // Sort by created_at (newest first) and take top 1K
224 usort($all_records, function($a, $b) {
225 return $b->created_at - $a->created_at;
226 });
227
228 $recent_1k = array_slice($all_records, 0, 1000);
229
230 // NEW: After getting records from Pinecone, check for role restrictions stored separately in WordPress
231 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
232 foreach ($recent_1k as &$record) {
233 // First try to get role restriction from Pinecone metadata (for new content)
234 if (empty($record->role_restriction) || $record->role_restriction === 'public') {
235 // If not in Pinecone metadata, check WordPress table (for user-modified roles)
236 $stored_role = $wpdb->get_var($wpdb->prepare(
237 "SELECT role_restriction FROM {$roles_table} WHERE vector_id = %s",
238 $record->id
239 ));
240
241 if ($stored_role) {
242 $record->role_restriction = $stored_role;
243 }
244 }
245 }
246
247 //error_log('DEBUG: Found ' . count($all_records) . ' total unique records, returning top ' . count($recent_1k));
248
249 // Cache the results for consistency within the same session
250 set_transient('mxchat_pinecone_recent_1k_cache', $recent_1k, 300); // Cache for 5 minutes
251
252 return $recent_1k;
253
254 } catch (Exception $e) {
255 //error_log('DEBUG: Exception in get_recent_1k_entries: ' . $e->getMessage());
256 return array();
257 }
258 }
259
260 /**
261 * Generate fixed query vectors for consistent results
262 */
263 private function mxchat_generate_fixed_query_vectors() {
264 $dimensions = $this->mxchat_get_embedding_dimensions();
265 $vectors = array();
266
267 // Create 5 fixed vectors with different patterns for better coverage
268 $patterns = array(
269 'zeros_with_ones' => 0.1, // Mostly zeros with some 1s
270 'ascending' => 0.2, // Ascending pattern
271 'descending' => 0.3, // Descending pattern
272 'alternating' => 0.4, // Alternating positive/negative
273 'center_weighted' => 0.5 // Higher values in center
274 );
275
276 foreach ($patterns as $pattern_name => $seed) {
277 $vector = array();
278
279 for ($i = 0; $i < $dimensions; $i++) {
280 switch ($pattern_name) {
281 case 'zeros_with_ones':
282 $vector[] = ($i % 10 === 0) ? 1.0 : 0.0;
283 break;
284 case 'ascending':
285 $vector[] = ($i / $dimensions) * 2 - 1; // Range -1 to 1
286 break;
287 case 'descending':
288 $vector[] = (($dimensions - $i) / $dimensions) * 2 - 1;
289 break;
290 case 'alternating':
291 $vector[] = ($i % 2 === 0) ? $seed : -$seed;
292 break;
293 case 'center_weighted':
294 $center = $dimensions / 2;
295 $distance = abs($i - $center) / $center;
296 $vector[] = (1 - $distance) * $seed;
297 break;
298 }
299 }
300
301 // Normalize the vector to unit length
302 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $vector)));
303 if ($magnitude > 0) {
304 $vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $vector);
305 }
306
307 $vectors[] = $vector;
308 }
309
310 return $vectors;
311 }
312
313
314 /**
315 * Scan Pinecone for processed content (MISSING FUNCTION - ADD THIS)
316 */
317 public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
318 //error_log('=== DEBUG: Starting mxchat_scan_pinecone_for_processed_content ===');
319
320 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
321 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
322
323 if (empty($api_key) || empty($host)) {
324 //error_log('DEBUG: Missing API credentials for scanning');
325 return array();
326 }
327
328 try {
329 // Use multiple random vectors to get better coverage
330 $all_matches = array();
331 $seen_ids = array();
332
333 // Try 3 different random vectors to get better coverage
334 for ($i = 0; $i < 3; $i++) {
335 //error_log('DEBUG: Scanning attempt ' . ($i + 1) . '/3');
336
337 $query_url = "https://{$host}/query";
338
339 // Generate random vector with CORRECT dimensions
340 $random_vector = $this->mxchat_generate_random_vector();
341
342 $query_data = array(
343 'includeMetadata' => true,
344 'includeValues' => false,
345 'topK' => 10000,
346 'vector' => $random_vector
347 );
348
349 $response = wp_remote_post($query_url, array(
350 'headers' => array(
351 'Api-Key' => $api_key,
352 'Content-Type' => 'application/json'
353 ),
354 'body' => json_encode($query_data),
355 'timeout' => 30
356 ));
357
358 if (is_wp_error($response)) {
359 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' WP error: ' . $response->get_error_message());
360 continue;
361 }
362
363 $response_code = wp_remote_retrieve_response_code($response);
364 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' response code: ' . $response_code);
365
366 if ($response_code !== 200) {
367 $error_body = wp_remote_retrieve_body($response);
368 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' failed with body: ' . substr($error_body, 0, 500));
369 continue;
370 }
371
372 $body = wp_remote_retrieve_body($response);
373 $data = json_decode($body, true);
374
375 if (isset($data['matches'])) {
376 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' returned ' . count($data['matches']) . ' matches');
377 foreach ($data['matches'] as $match) {
378 $match_id = $match['id'] ?? '';
379 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
380 $all_matches[] = $match;
381 $seen_ids[$match_id] = true;
382 }
383 }
384 } else {
385 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' - no matches key in response');
386 }
387 }
388
389 //error_log('DEBUG: Total unique matches found: ' . count($all_matches));
390
391 // Convert matches to processed data format
392 $processed_data = array();
393 $vector_ids_for_cache = array();
394
395 foreach ($all_matches as $match) {
396 $metadata = $match['metadata'] ?? array();
397 $source_url = $metadata['source_url'] ?? '';
398 $match_id = $match['id'] ?? '';
399
400 if (!empty($source_url) && !empty($match_id)) {
401 $post_id = url_to_postid($source_url);
402 if ($post_id) {
403 $created_at = $metadata['created_at'] ?? '';
404 $processed_date = 'Recently';
405
406 if (!empty($created_at)) {
407 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
408 if ($timestamp) {
409 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
410 }
411 }
412
413 $processed_data[$post_id] = array(
414 'db_id' => $match_id,
415 'processed_date' => $processed_date,
416 'url' => $source_url,
417 'source' => 'pinecone',
418 'timestamp' => $timestamp ?? current_time('timestamp')
419 );
420
421 $vector_ids_for_cache[] = $match_id;
422 }
423 }
424 }
425
426 // Update the vector IDs cache for future use
427 if (!empty($vector_ids_for_cache)) {
428 update_option('mxchat_pinecone_vector_ids_cache', $vector_ids_for_cache);
429 //error_log('DEBUG: Updated vector IDs cache with ' . count($vector_ids_for_cache) . ' IDs');
430 }
431
432 //error_log('DEBUG: Returning ' . count($processed_data) . ' processed items from scanning');
433 return $processed_data;
434
435 } catch (Exception $e) {
436 //error_log('DEBUG: Exception in scan_pinecone_for_processed_content: ' . $e->getMessage());
437 return array();
438 }
439 }
440
441 /**
442 * Get total count of vectors in Pinecone (SIMPLE VERSION)
443 */
444 private function mxchat_get_pinecone_total_count($pinecone_options) {
445 // First try the stats API
446 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
447 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
448
449 if (empty($api_key) || empty($host)) {
450 return 0;
451 }
452
453 try {
454 $stats_url = "https://{$host}/describe_index_stats";
455
456 // Try GET request
457 $response = wp_remote_get($stats_url, array(
458 'headers' => array(
459 'Api-Key' => $api_key,
460 'Accept' => 'application/json'
461 ),
462 'timeout' => 15
463 ));
464
465 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
466 $body = wp_remote_retrieve_body($response);
467 $stats_data = json_decode($body, true);
468
469 $total_count = $stats_data['totalVectorCount'] ?? 0;
470 if ($total_count > 0) {
471 //error_log('DEBUG: Got total count from stats API: ' . $total_count);
472 return intval($total_count);
473 }
474 }
475
476 // Fallback: estimate from previous scans
477 $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
478 if (!empty($cached_vector_ids)) {
479 $estimated_count = count($cached_vector_ids);
480 //error_log('DEBUG: Using estimated count from cache: ' . $estimated_count);
481 return intval($estimated_count);
482 }
483
484 } catch (Exception $e) {
485 //error_log('DEBUG: Exception getting total count: ' . $e->getMessage());
486 }
487
488 // If all else fails, return 0
489 return 0;
490 }
491
492 /**
493 * Call this after adding new content to refresh the view
494 */
495 public function mxchat_refresh_after_new_content($pinecone_options) {
496 //error_log('DEBUG: Refreshing after new content added');
497
498 // Clear all relevant caches
499 delete_transient('mxchat_pinecone_recent_1k_cache'); // Add this line
500 delete_transient('mxchat_pinecone_recent_1k');
501 delete_transient('mxchat_pinecone_total_count');
502
503 // Force fresh fetch on next page load
504 return true;
505 }
506
507
508 /**
509 * Fetches vectors from Pinecone using provided IDs (for content selection feature)
510 */
511 public function fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
512 //error_log('=== DEBUG: fetch_pinecone_vectors_by_ids started (content selection method) ===');
513
514 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
515 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
516
517 //error_log('DEBUG: API key present: ' . (!empty($api_key) ? 'YES' : 'NO'));
518 //error_log('DEBUG: Host: ' . $host);
519 //error_log('DEBUG: Vector IDs count: ' . count($vector_ids));
520
521 if (empty($api_key) || empty($host) || empty($vector_ids)) {
522 //error_log('DEBUG: Missing parameters for fetch by IDs (content selection)');
523 return array();
524 }
525
526 try {
527 $fetch_url = "https://{$host}/vectors/fetch";
528 //error_log('DEBUG: Fetch URL: ' . $fetch_url);
529
530 // Pinecone fetch API allows fetching specific vectors by ID
531 $fetch_data = array(
532 'ids' => array_values($vector_ids)
533 );
534
535 $response = wp_remote_post($fetch_url, array(
536 'headers' => array(
537 'Api-Key' => $api_key,
538 'Content-Type' => 'application/json'
539 ),
540 'body' => json_encode($fetch_data),
541 'timeout' => 30
542 ));
543
544 if (is_wp_error($response)) {
545 //error_log('DEBUG: Fetch by IDs WP error (content selection): ' . $response->get_error_message());
546 return array();
547 }
548
549 $response_code = wp_remote_retrieve_response_code($response);
550 //error_log('DEBUG: Fetch response code (content selection): ' . $response_code);
551
552 if ($response_code !== 200) {
553 $error_body = wp_remote_retrieve_body($response);
554 //error_log('DEBUG: Fetch failed with body (content selection): ' . $error_body);
555 return array();
556 }
557
558 $body = wp_remote_retrieve_body($response);
559 $data = json_decode($body, true);
560
561 if (!isset($data['vectors'])) {
562 //error_log('DEBUG: No vectors key in response (content selection)');
563 return array();
564 }
565
566 $processed_data = array();
567
568 foreach ($data['vectors'] as $vector_id => $vector_data) {
569 $metadata = $vector_data['metadata'] ?? array();
570 $source_url = $metadata['source_url'] ?? '';
571
572 if (!empty($source_url)) {
573 $post_id = url_to_postid($source_url);
574 if ($post_id) {
575 $created_at = $metadata['created_at'] ?? '';
576 $processed_date = 'Recently'; // Default
577
578 if (!empty($created_at)) {
579 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
580 if ($timestamp) {
581 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
582 }
583 }
584
585 $processed_data[$post_id] = array(
586 'db_id' => $vector_id,
587 'processed_date' => $processed_date,
588 'url' => $source_url,
589 'source' => 'pinecone',
590 'timestamp' => $timestamp ?? current_time('timestamp')
591 );
592 }
593 }
594 }
595
596 //error_log('DEBUG: Processed ' . count($processed_data) . ' records (content selection method)');
597 //error_log('=== DEBUG: fetch_pinecone_vectors_by_ids completed (content selection) ===');
598
599 return $processed_data;
600
601 } catch (Exception $e) {
602 //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids (content selection): ' . $e->getMessage());
603 return array();
604 }
605 }
606
607
608 // ========================================
609 // PINECONE DELETE OPERATIONS
610 // ========================================
611
612 public function mxchat_delete_all_from_pinecone($pinecone_options) {
613 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
614 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
615
616 if (empty($api_key) || empty($host)) {
617 return array(
618 'success' => false,
619 'message' => 'Missing Pinecone API credentials'
620 );
621 }
622
623 try {
624 // First, get all vector IDs
625 $all_vector_ids = array();
626
627 // Try to get from cache first
628 $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
629 if (!empty($cached_vector_ids)) {
630 $all_vector_ids = $cached_vector_ids;
631 } else {
632 // Use the correct method name that exists in your class
633 $records = $this->mxchat_get_recent_1k_entries($pinecone_options);
634 foreach ($records as $record) {
635 if (!empty($record->id)) {
636 $all_vector_ids[] = $record->id;
637 }
638 }
639 }
640
641 if (empty($all_vector_ids)) {
642 return array(
643 'success' => true,
644 'message' => 'No vectors found to delete'
645 );
646 }
647
648 // Delete vectors in batches (Pinecone has limits on batch operations)
649 $batch_size = 100;
650 $batches = array_chunk($all_vector_ids, $batch_size);
651 $deleted_count = 0;
652 $failed_batches = 0;
653
654 foreach ($batches as $batch) {
655 $result = $this->mxchat_delete_pinecone_batch($batch, $api_key, $host);
656 if ($result['success']) {
657 $deleted_count += count($batch);
658 } else {
659 $failed_batches++;
660 //error_log('Failed to delete Pinecone batch: ' . $result['message']);
661 }
662 }
663
664 // CLEAR ALL RELEVANT CACHES - EXACTLY like your single delete
665 delete_transient('mxchat_pinecone_recent_1k_cache');
666 delete_option('mxchat_pinecone_vector_ids_cache');
667 delete_option('mxchat_pinecone_processed_cache');
668 delete_option('mxchat_processed_content_cache');
669
670 // Also force refresh for next page load - EXACTLY like your single delete
671 $this->mxchat_refresh_after_new_content($pinecone_options);
672
673 if ($failed_batches > 0) {
674 return array(
675 'success' => false,
676 'message' => sprintf('Deleted %d vectors, but %d batches failed', $deleted_count, $failed_batches)
677 );
678 }
679
680 return array(
681 'success' => true,
682 'message' => "Successfully deleted {$deleted_count} vectors from Pinecone"
683 );
684
685 } catch (Exception $e) {
686 //error_log('Pinecone delete all exception: ' . $e->getMessage());
687 return array(
688 'success' => false,
689 'message' => $e->getMessage()
690 );
691 }
692 }
693
694
695 /**
696 * Deletes batch of vectors from Pinecone database
697 */
698 private function mxchat_delete_pinecone_batch($vector_ids, $api_key, $host) {
699 // Build the API endpoint
700 $api_endpoint = "https://{$host}/vectors/delete";
701
702 // Prepare the request body with the IDs
703 $request_body = array(
704 'ids' => $vector_ids
705 );
706
707 // Make the deletion request
708 $response = wp_remote_post($api_endpoint, array(
709 'headers' => array(
710 'Api-Key' => $api_key,
711 'accept' => 'application/json',
712 'content-type' => 'application/json'
713 ),
714 'body' => wp_json_encode($request_body),
715 'timeout' => 60, // Increased timeout for batch operations
716 'method' => 'POST'
717 ));
718
719 // Handle WordPress HTTP API errors
720 if (is_wp_error($response)) {
721 return array(
722 'success' => false,
723 'message' => $response->get_error_message()
724 );
725 }
726
727 // Check response status
728 $response_code = wp_remote_retrieve_response_code($response);
729 $response_body = wp_remote_retrieve_body($response);
730
731 // Pinecone returns 200 for successful deletion
732 if ($response_code !== 200) {
733 //error_log('Pinecone batch deletion failed: HTTP ' . $response_code . ' - ' . $response_body);
734 return array(
735 'success' => false,
736 'message' => sprintf(
737 'Pinecone API error (HTTP %d): %s',
738 $response_code,
739 $response_body
740 )
741 );
742 }
743
744 return array(
745 'success' => true,
746 'message' => 'Batch deleted successfully from Pinecone'
747 );
748 }
749
750
751 /**
752 * Deletes vector from Pinecone using API request
753 */
754 public function mxchat_delete_from_pinecone_by_vector_id($vector_id, $api_key, $host) {
755 // Build the API endpoint
756 $api_endpoint = "https://{$host}/vectors/delete";
757
758 // Prepare the request body with just the ID
759 $request_body = array(
760 'ids' => array($vector_id)
761 );
762
763 // Make the deletion request
764 $response = wp_remote_post($api_endpoint, array(
765 'headers' => array(
766 'Api-Key' => $api_key,
767 'accept' => 'application/json',
768 'content-type' => 'application/json'
769 ),
770 'body' => wp_json_encode($request_body),
771 'timeout' => 30,
772 'method' => 'POST'
773 ));
774
775 // Handle WordPress HTTP API errors
776 if (is_wp_error($response)) {
777 return array(
778 'success' => false,
779 'message' => $response->get_error_message()
780 );
781 }
782
783 // Check response status
784 $response_code = wp_remote_retrieve_response_code($response);
785 $response_body = wp_remote_retrieve_body($response);
786
787 // Pinecone returns 200 for successful deletion
788 if ($response_code !== 200) {
789 return array(
790 'success' => false,
791 'message' => sprintf(
792 'Pinecone API error (HTTP %d): %s',
793 $response_code,
794 $response_body
795 )
796 );
797 }
798
799 return array(
800 'success' => true,
801 'message' => 'Vector deleted successfully from Pinecone'
802 );
803 }
804 /**
805 * Deletes data from Pinecone using a source URL
806 */
807 public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
808 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
809 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
810
811 if (empty($host) || empty($api_key)) {
812 //error_log('MXChat: Pinecone deletion failed - missing configuration');
813 return false;
814 }
815
816 $api_endpoint = "https://{$host}/vectors/delete";
817 $vector_id = md5($source_url);
818
819 $request_body = array(
820 'ids' => array($vector_id)
821 );
822
823 $response = wp_remote_post($api_endpoint, array(
824 'headers' => array(
825 'Api-Key' => $api_key,
826 'accept' => 'application/json',
827 'content-type' => 'application/json'
828 ),
829 'body' => wp_json_encode($request_body),
830 'timeout' => 30
831 ));
832
833 if (is_wp_error($response)) {
834 //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
835 return false;
836 }
837
838 $response_code = wp_remote_retrieve_response_code($response);
839 if ($response_code !== 200) {
840 //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
841 return false;
842 }
843
844 return true;
845 }
846
847
848 /**
849 * Deletes data from Pinecone index using API key
850 */
851 private function mxchat_delete_from_pinecone($urls, $api_key, $environment, $index_name) {
852 // Get the Pinecone host from options (matching your store_in_pinecone_main pattern)
853 $options = get_option('mxchat_pinecone_addon_options');
854 $host = $options['mxchat_pinecone_host'] ?? '';
855
856 if (empty($host)) {
857 return array(
858 'success' => false,
859 'message' => 'Pinecone host is not configured. Please set the host in your settings.'
860 );
861 }
862
863 // Build API endpoint using the configured host
864 $api_endpoint = "https://{$host}/vectors/delete";
865
866 // Create vector IDs from URLs (matching your store method's ID generation)
867 $vector_ids = array_map('md5', $urls);
868
869 // Prepare the delete request body
870 $request_body = array(
871 'ids' => $vector_ids,
872 'filter' => array(
873 'source_url' => array(
874 '$in' => $urls
875 )
876 )
877 );
878
879 // Make the deletion request
880 $response = wp_remote_post($api_endpoint, array(
881 'headers' => array(
882 'Api-Key' => $api_key,
883 'accept' => 'application/json',
884 'content-type' => 'application/json'
885 ),
886 'body' => wp_json_encode($request_body),
887 'timeout' => 30,
888 'data_format' => 'body'
889 ));
890
891 // Handle WordPress HTTP API errors
892 if (is_wp_error($response)) {
893 return array(
894 'success' => false,
895 'message' => $response->get_error_message()
896 );
897 }
898
899 // Check response status
900 $response_code = wp_remote_retrieve_response_code($response);
901 if ($response_code !== 200) {
902 $body = wp_remote_retrieve_body($response);
903 return array(
904 'success' => false,
905 'message' => sprintf(
906 'Pinecone API error (HTTP %d): %s',
907 $response_code,
908 $body
909 )
910 );
911 }
912
913 // Parse response body
914 $body = wp_remote_retrieve_body($response);
915 $response_data = json_decode($body, true);
916
917 // Final validation of the response
918 if (json_last_error() !== JSON_ERROR_NONE) {
919 return array(
920 'success' => false,
921 'message' => 'Failed to parse Pinecone response: ' . json_last_error_msg()
922 );
923 }
924
925 return array(
926 'success' => true,
927 'message' => sprintf('Successfully deleted %d vectors from Pinecone', count($vector_ids))
928 );
929 }
930
931
932 // ========================================
933 // VECTOR CACHE MANAGEMENT
934 // ========================================
935
936
937 /**
938 * Removes vector ID from cache array option
939 */
940 public function mxchat_remove_from_pinecone_vector_cache($vector_id) {
941 $cached_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
942 $key = array_search($vector_id, $cached_ids);
943 if ($key !== false) {
944 unset($cached_ids[$key]);
945 update_option('mxchat_pinecone_vector_ids_cache', array_values($cached_ids));
946 }
947 }
948
949 /**
950 * Removes vector ID from processed content caches
951 */
952 public function mxchat_remove_from_processed_content_caches($vector_id) {
953 // Get all caches
954 $pinecone_cache = get_option('mxchat_pinecone_processed_cache', array());
955 $processed_cache = get_option('mxchat_processed_content_cache', array());
956
957 // We need to find the post ID that corresponds to this vector ID
958 // Vector ID is typically md5 of the source URL
959 $post_id_to_remove = null;
960
961 // Search through caches to find matching post
962 foreach ($pinecone_cache as $post_id => $cache_data) {
963 if (isset($cache_data['db_id']) && $cache_data['db_id'] === $vector_id) {
964 $post_id_to_remove = $post_id;
965 break;
966 }
967 }
968
969 // Also check the processed cache
970 if (!$post_id_to_remove) {
971 foreach ($processed_cache as $post_id => $cache_data) {
972 if (isset($cache_data['db_id']) && $cache_data['db_id'] === $vector_id) {
973 $post_id_to_remove = $post_id;
974 break;
975 }
976 }
977 }
978
979 // If we found the post ID, remove it from both caches
980 if ($post_id_to_remove) {
981 unset($pinecone_cache[$post_id_to_remove]);
982 unset($processed_cache[$post_id_to_remove]);
983
984 update_option('mxchat_pinecone_processed_cache', $pinecone_cache);
985 update_option('mxchat_processed_content_cache', $processed_cache);
986
987 //error_log('Removed post ID ' . $post_id_to_remove . ' from processed content caches');
988 } else {
989 // If we can't find by vector ID, we might need to reconstruct the URL
990 // and find the post ID that way
991 //error_log('Could not find post ID for vector ID: ' . $vector_id);
992 }
993 }
994
995
996 /**
997 * Retrieves and caches Pinecone API processed content
998 */
999 public function mxchat_get_pinecone_processed_content($pinecone_options) {
1000 // First check local cache for immediate updates
1001 $cached_data = get_option('mxchat_pinecone_processed_cache', array());
1002
1003 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1004 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1005
1006 if (empty($api_key) || empty($host)) {
1007 // Return only cached data if API credentials are missing
1008 return $cached_data;
1009 }
1010
1011 $pinecone_data = array();
1012
1013 try {
1014 // Method 1: Try to get vectors using cached vector IDs first
1015 $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
1016
1017 if (!empty($cached_vector_ids)) {
1018 $pinecone_data = $this->fetch_pinecone_vectors_by_ids($pinecone_options, $cached_vector_ids);
1019 }
1020
1021 // Method 2: If no cached IDs or fetch failed, use scanning approach
1022 if (empty($pinecone_data)) {
1023 $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
1024 }
1025
1026 // Method 3: Final fallback - try stats endpoint (if available)
1027 if (empty($pinecone_data)) {
1028 $stats_url = "https://{$host}/describe_index_stats";
1029
1030 $response = wp_remote_post($stats_url, array(
1031 'headers' => array(
1032 'Api-Key' => $api_key,
1033 'Content-Type' => 'application/json'
1034 ),
1035 'body' => json_encode(array()),
1036 'timeout' => 30
1037 ));
1038
1039 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1040 $body = wp_remote_retrieve_body($response);
1041 $stats_data = json_decode($body, true);
1042
1043 // Log stats for debugging but don't rely on them for vector listing
1044 //error_log('Pinecone index stats: ' . print_r($stats_data, true));
1045 }
1046 }
1047
1048 } catch (Exception $e) {
1049 //error_log('Pinecone processed content exception: ' . $e->getMessage());
1050 }
1051
1052 // Merge cached data with Pinecone data
1053 // Cache takes priority for recent updates (within last 5 minutes)
1054 $merged_data = $pinecone_data;
1055
1056 foreach ($cached_data as $post_id => $cache_item) {
1057 $cache_timestamp = $cache_item['timestamp'] ?? 0;
1058 $time_diff = current_time('timestamp') - $cache_timestamp;
1059
1060 // If cache item is recent (less than 5 minutes), prioritize it
1061 if ($time_diff < 300) { // 5 minutes = 300 seconds
1062 $merged_data[$post_id] = $cache_item;
1063 } else {
1064 // If not in Pinecone data and cache is old, keep cache but mark as potentially stale
1065 if (!isset($merged_data[$post_id])) {
1066 $merged_data[$post_id] = $cache_item;
1067 }
1068 }
1069 }
1070
1071 return $merged_data;
1072 }
1073
1074
1075 // ========================================
1076 // HELPER METHODS
1077 // ========================================
1078
1079 /**
1080 * Validates Pinecone API credentials
1081 */
1082 private function mxchat_validate_pinecone_credentials($api_key, $host) {
1083 if (empty($api_key) || empty($host)) {
1084 return false;
1085 }
1086 return true;
1087 }
1088
1089 /**
1090 * Get Pinecone API credentials from options
1091 */
1092 private function mxchat_get_pinecone_credentials() {
1093 $options = get_option('mxchat_options', array());
1094 return array(
1095 'api_key' => isset($options['pinecone_api_key']) ? $options['pinecone_api_key'] : '',
1096 'host' => isset($options['pinecone_host']) ? $options['pinecone_host'] : ''
1097 );
1098 }
1099
1100 /**
1101 * Log Pinecone operation errors
1102 */
1103 private function log_pinecone_error($operation, $error_message) {
1104 //error_log("MxChat Pinecone {$operation} Error: " . $error_message);
1105 }
1106
1107 // ========================================
1108 // STATIC ACCESS METHODS (for backward compatibility)
1109 // ========================================
1110
1111 /**
1112 * Get singleton instance
1113 */
1114 public static function get_instance() {
1115 static $instance = null;
1116 if ($instance === null) {
1117 $instance = new self();
1118 }
1119 return $instance;
1120 }
1121 }
1122
1123 // Initialize the Pinecone manager
1124 $mxchat_pinecone_manager = MxChat_Pinecone_Manager::get_instance();
1125