PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.3.0
MxChat – AI Chatbot & Content Generation for WordPress v2.3.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.3.0, at admin/class-pinecone-manager.php

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