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

1,106 lines 39.4 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 with bot-specific filtering
29 */
30 public function mxchat_fetch_pinecone_records($pinecone_options, $search_query = '', $page = 1, $per_page = 20, $bot_id = 'default') {
31 //error_log('=== DEBUG: mxchat_fetch_pinecone_records ===');
32 //error_log('Bot ID: ' . $bot_id);
33 //error_log('Pinecone Host: ' . ($pinecone_options['mxchat_pinecone_host'] ?? 'NOT SET'));
34 //error_log('Pinecone Namespace: ' . ($pinecone_options['mxchat_pinecone_namespace'] ?? 'NOT SET'));
35 //error_log('Use Pinecone: ' . ($pinecone_options['mxchat_use_pinecone'] ?? 'NOT SET'));
36
37 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
38 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
39 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
40
41 if (empty($api_key) || empty($host)) {
42 return array('data' => array(), 'total' => 0, 'total_in_database' => 0, 'showing_recent_only' => false);
43 }
44
45 try {
46 // Get total count for the banner message (bot-specific)
47 $total_in_database = $this->mxchat_get_pinecone_total_count($pinecone_options, $bot_id);
48
49 // Always get fresh data - no caching
50 $all_records = $this->mxchat_get_recent_1k_entries($pinecone_options, $bot_id);
51
52 // Filter by search query if provided
53 if (!empty($search_query)) {
54 $all_records = array_filter($all_records, function($record) use ($search_query) {
55 $content = $record->article_content ?? '';
56 $source_url = $record->source_url ?? '';
57 return stripos($content, $search_query) !== false || stripos($source_url, $search_query) !== false;
58 });
59 }
60
61 // Handle pagination
62 $total = count($all_records);
63 $offset = ($page - 1) * $per_page;
64 $paged_records = array_slice($all_records, $offset, $per_page);
65
66 return array(
67 'data' => $paged_records,
68 'total' => $total,
69 'total_in_database' => $total_in_database,
70 'showing_recent_only' => ($total_in_database > 1000)
71 );
72
73 } catch (Exception $e) {
74 return array('data' => array(), 'total' => 0, 'total_in_database' => 0, 'showing_recent_only' => false);
75 }
76 }
77 /**
78 * Get embedding dimensions based on the selected model
79 * ADD THIS NEW FUNCTION
80 */
81 private function mxchat_get_embedding_dimensions() {
82 $options = get_option('mxchat_options', array());
83 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
84
85 // Define dimensions for different models
86 $model_dimensions = array(
87 'text-embedding-ada-002' => 1536,
88 'text-embedding-3-small' => 1536,
89 'text-embedding-3-large' => 3072,
90 'voyage-2' => 1024,
91 'voyage-large-2' => 1536,
92 'voyage-3-large' => 2048,
93 'gemini-embedding-001' => 1536,
94 );
95
96 // Check if it's a voyage model with custom dimensions
97 if (strpos($selected_model, 'voyage-3-large') === 0) {
98 $custom_dimensions = $options['voyage_output_dimension'] ?? 2048;
99 return intval($custom_dimensions);
100 }
101
102 // Check if it's a gemini model with custom dimensions
103 if (strpos($selected_model, 'gemini-embedding') === 0) {
104 $custom_dimensions = $options['gemini_output_dimension'] ?? 1536;
105 return intval($custom_dimensions);
106 }
107
108 // Return known dimensions or default to 1536
109 return $model_dimensions[$selected_model] ?? 1536;
110 }
111
112 /**
113 * Generate random unit vector with correct dimensions
114 * ADD THIS NEW FUNCTION
115 */
116 private function mxchat_generate_random_vector() {
117 $dimensions = $this->mxchat_get_embedding_dimensions();
118
119 $random_vector = array();
120 for ($i = 0; $i < $dimensions; $i++) {
121 $random_vector[] = (rand(-1000, 1000) / 1000.0);
122 }
123
124 // Normalize the vector to unit length
125 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
126 if ($magnitude > 0) {
127 $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
128 }
129
130 return $random_vector;
131 }
132
133
134 /**
135 * Get recent 1K entries from Pinecone
136 */
137 private function mxchat_get_recent_1k_entries($pinecone_options, $bot_id = 'default') {
138 global $wpdb;
139 //error_log('=== DEBUG: mxchat_get_recent_1k_entries started ===');
140 //error_log('DEBUG: Bot ID: ' . $bot_id);
141
142 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
143 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
144 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
145
146 if (empty($api_key) || empty($host)) {
147 return array();
148 }
149
150 try {
151 $all_records = array();
152 $seen_ids = array();
153 $query_url = "https://{$host}/query";
154
155 $fixed_vectors = $this->mxchat_generate_fixed_query_vectors();
156
157 foreach ($fixed_vectors as $vector_index => $query_vector) {
158 //error_log('DEBUG: Using fixed query vector ' . ($vector_index + 1) . '/' . count($fixed_vectors));
159
160 $query_data = array(
161 'includeMetadata' => true,
162 'includeValues' => false,
163 'topK' => 3000,
164 'vector' => $query_vector
165 );
166
167 // Add namespace if provided
168 if (!empty($namespace)) {
169 $query_data['namespace'] = $namespace;
170 }
171
172 $response = wp_remote_post($query_url, array(
173 'headers' => array(
174 'Api-Key' => $api_key,
175 'Content-Type' => 'application/json'
176 ),
177 'body' => json_encode($query_data),
178 'timeout' => 30
179 ));
180
181 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
182 $body = wp_remote_retrieve_body($response);
183 $data = json_decode($body, true);
184
185 if (isset($data['matches'])) {
186 foreach ($data['matches'] as $match) {
187 $match_id = $match['id'] ?? '';
188 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
189 $metadata = $match['metadata'] ?? array();
190
191 // Get created_at timestamp
192 $created_at = $metadata['created_at'] ??
193 $metadata['last_updated'] ??
194 $metadata['timestamp'] ??
195 time();
196
197 if (!is_numeric($created_at)) {
198 $created_at = strtotime($created_at) ?: time();
199 }
200
201 $all_records[] = (object) array(
202 'id' => $match_id,
203 'article_content' => $metadata['text'] ?? '',
204 'source_url' => $metadata['source_url'] ?? '',
205 'role_restriction' => $metadata['role_restriction'] ?? 'public',
206 'bot_id' => $bot_id,
207 'created_at' => $created_at,
208 'data_source' => 'pinecone'
209 );
210
211 $seen_ids[$match_id] = true;
212 }
213 }
214 }
215 }
216
217 usleep(200000); // 0.2 second delay
218 }
219
220 // Sort by created_at (newest first) and take top 1K
221 usort($all_records, function($a, $b) {
222 return $b->created_at - $a->created_at;
223 });
224
225 $recent_1k = array_slice($all_records, 0, 1000);
226
227 // Check for role restrictions stored in WordPress
228 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
229 foreach ($recent_1k as &$record) {
230 if (empty($record->role_restriction) || $record->role_restriction === 'public') {
231 $stored_role = $wpdb->get_var($wpdb->prepare(
232 "SELECT role_restriction FROM {$roles_table} WHERE vector_id = %s AND bot_id = %s",
233 $record->id,
234 $bot_id
235 ));
236
237 if ($stored_role) {
238 $record->role_restriction = $stored_role;
239 }
240 }
241 }
242
243 //error_log('DEBUG: Found ' . count($all_records) . ' total unique records, returning top ' . count($recent_1k));
244
245 return $recent_1k;
246
247 } catch (Exception $e) {
248 //error_log('DEBUG: Exception in get_recent_1k_entries: ' . $e->getMessage());
249 return array();
250 }
251 }
252 /**
253 * Generate fixed query vectors for consistent results
254 */
255 private function mxchat_generate_fixed_query_vectors() {
256 $dimensions = $this->mxchat_get_embedding_dimensions();
257 $vectors = array();
258
259 // Create 5 fixed vectors with different patterns for better coverage
260 $patterns = array(
261 'zeros_with_ones' => 0.1, // Mostly zeros with some 1s
262 'ascending' => 0.2, // Ascending pattern
263 'descending' => 0.3, // Descending pattern
264 'alternating' => 0.4, // Alternating positive/negative
265 'center_weighted' => 0.5 // Higher values in center
266 );
267
268 foreach ($patterns as $pattern_name => $seed) {
269 $vector = array();
270
271 for ($i = 0; $i < $dimensions; $i++) {
272 switch ($pattern_name) {
273 case 'zeros_with_ones':
274 $vector[] = ($i % 10 === 0) ? 1.0 : 0.0;
275 break;
276 case 'ascending':
277 $vector[] = ($i / $dimensions) * 2 - 1; // Range -1 to 1
278 break;
279 case 'descending':
280 $vector[] = (($dimensions - $i) / $dimensions) * 2 - 1;
281 break;
282 case 'alternating':
283 $vector[] = ($i % 2 === 0) ? $seed : -$seed;
284 break;
285 case 'center_weighted':
286 $center = $dimensions / 2;
287 $distance = abs($i - $center) / $center;
288 $vector[] = (1 - $distance) * $seed;
289 break;
290 }
291 }
292
293 // Normalize the vector to unit length
294 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $vector)));
295 if ($magnitude > 0) {
296 $vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $vector);
297 }
298
299 $vectors[] = $vector;
300 }
301
302 return $vectors;
303 }
304
305
306 /**
307 * Scan Pinecone for processed content
308 */
309 public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
310 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
311 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
312
313 if (empty($api_key) || empty($host)) {
314 return array();
315 }
316
317 try {
318 // Use multiple random vectors to get better coverage
319 $all_matches = array();
320 $seen_ids = array();
321
322 // Try 3 different random vectors to get better coverage
323 for ($i = 0; $i < 3; $i++) {
324 $query_url = "https://{$host}/query";
325
326 // Generate random vector with CORRECT dimensions
327 $random_vector = $this->mxchat_generate_random_vector();
328
329 $query_data = array(
330 'includeMetadata' => true,
331 'includeValues' => false,
332 'topK' => 10000,
333 'vector' => $random_vector
334 );
335
336 $response = wp_remote_post($query_url, array(
337 'headers' => array(
338 'Api-Key' => $api_key,
339 'Content-Type' => 'application/json'
340 ),
341 'body' => json_encode($query_data),
342 'timeout' => 30
343 ));
344
345 if (is_wp_error($response)) {
346 continue;
347 }
348
349 $response_code = wp_remote_retrieve_response_code($response);
350
351 if ($response_code !== 200) {
352 continue;
353 }
354
355 $body = wp_remote_retrieve_body($response);
356 $data = json_decode($body, true);
357
358 if (isset($data['matches'])) {
359 foreach ($data['matches'] as $match) {
360 $match_id = $match['id'] ?? '';
361 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
362 $all_matches[] = $match;
363 $seen_ids[$match_id] = true;
364 }
365 }
366 }
367 }
368
369 // Convert matches to processed data format
370 $processed_data = array();
371
372 foreach ($all_matches as $match) {
373 $metadata = $match['metadata'] ?? array();
374 $source_url = $metadata['source_url'] ?? '';
375 $match_id = $match['id'] ?? '';
376
377 if (!empty($source_url) && !empty($match_id)) {
378 $post_id = url_to_postid($source_url);
379 if ($post_id) {
380 $created_at = $metadata['created_at'] ?? '';
381 $processed_date = 'Recently';
382
383 if (!empty($created_at)) {
384 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
385 if ($timestamp) {
386 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
387 }
388 }
389
390 $processed_data[$post_id] = array(
391 'db_id' => $match_id,
392 'processed_date' => $processed_date,
393 'url' => $source_url,
394 'source' => 'pinecone',
395 'timestamp' => $timestamp ?? current_time('timestamp')
396 );
397 }
398 }
399 }
400
401 return $processed_data;
402
403 } catch (Exception $e) {
404 return array();
405 }
406 }
407
408 /**
409 * Get total count from Pinecone stats API
410 * UPDATED: Removed cache fallback reference
411 */
412 private function mxchat_get_pinecone_total_count($pinecone_options, $bot_id = 'default') {
413 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
414 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
415 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
416
417 if (empty($api_key) || empty($host)) {
418 return 0;
419 }
420
421 try {
422 $stats_url = "https://{$host}/describe_index_stats";
423
424 $request_data = array();
425
426 // Add namespace if provided
427 if (!empty($namespace)) {
428 $request_data['namespace'] = $namespace;
429 }
430
431 $response = wp_remote_post($stats_url, array(
432 'headers' => array(
433 'Api-Key' => $api_key,
434 'Content-Type' => 'application/json'
435 ),
436 'body' => json_encode($request_data),
437 'timeout' => 15
438 ));
439
440 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
441 $body = wp_remote_retrieve_body($response);
442 $stats_data = json_decode($body, true);
443
444 $total_count = $stats_data['totalVectorCount'] ?? 0;
445 if ($total_count > 0) {
446 //error_log('DEBUG: Got total count from stats API: ' . $total_count);
447 return intval($total_count);
448 }
449 }
450
451 // If stats API fails, return 0 instead of using cache
452 return 0;
453
454 } catch (Exception $e) {
455 //error_log('DEBUG: Exception getting total count: ' . $e->getMessage());
456 return 0;
457 }
458 }
459
460 /**
461 * Get bot-specific Pinecone configuration for database operations
462 */
463 public function mxchat_get_bot_pinecone_options($bot_id = 'default') {
464 //error_log('DEBUG: Getting Pinecone options for bot: ' . $bot_id);
465
466 // If default bot or multi-bot add-on not active, use default Pinecone config
467 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
468 $addon_options = get_option('mxchat_pinecone_addon_options', array());
469 //error_log('DEBUG: Using default Pinecone options');
470 return $addon_options;
471 }
472
473 // Get bot-specific configuration using the filter
474 $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
475
476 //error_log('DEBUG: Bot config from filter: ' . print_r($bot_config, true));
477
478 // Check if we got valid bot-specific config
479 if (!empty($bot_config) && isset($bot_config['use_pinecone']) && $bot_config['use_pinecone']) {
480 // Convert bot config to the format expected by fetch functions
481 $pinecone_options = array(
482 'mxchat_use_pinecone' => '1',
483 'mxchat_pinecone_api_key' => $bot_config['api_key'] ?? '',
484 'mxchat_pinecone_host' => $bot_config['host'] ?? '',
485 'mxchat_pinecone_namespace' => $bot_config['namespace'] ?? '',
486 'mxchat_pinecone_environment' => '',
487 'mxchat_pinecone_index' => ''
488 );
489
490 //error_log('DEBUG: Returning bot-specific Pinecone options for bot: ' . $bot_id);
491 return $pinecone_options;
492 }
493
494 // Fallback to default options if bot-specific config is invalid
495 //error_log('DEBUG: Bot-specific config invalid, falling back to default');
496 return get_option('mxchat_pinecone_addon_options', array());
497 }
498
499 /**
500 * Get bot-specific Pinecone configuration
501 * Used in the knowledge retrieval functions
502 */
503 private function get_bot_pinecone_config($bot_id = 'default') {
504 //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
505
506 // If default bot or multi-bot add-on not active, use default Pinecone config
507 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
508 //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
509 $addon_options = get_option('mxchat_pinecone_addon_options', array());
510 $config = array(
511 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
512 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
513 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
514 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
515 );
516 //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
517 return $config;
518 }
519
520 //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
521
522 // Hook for multi-bot add-on to provide bot-specific Pinecone config
523 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
524
525 if (!empty($bot_pinecone_config)) {
526 //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
527 //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
528 //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
529 //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
530 } else {
531 //error_log("MXCHAT DEBUG: Filter returned empty config!");
532 }
533
534 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
535 }
536
537
538 /**
539 * Fetches vectors from Pinecone using provided IDs (for content selection feature)
540 */
541 public function fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
542 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
543 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
544
545 if (empty($api_key) || empty($host) || empty($vector_ids)) {
546 return array();
547 }
548
549 try {
550 $fetch_url = "https://{$host}/vectors/fetch";
551
552 // Pinecone fetch API allows fetching specific vectors by ID
553 $fetch_data = array(
554 'ids' => array_values($vector_ids)
555 );
556
557 $response = wp_remote_post($fetch_url, array(
558 'headers' => array(
559 'Api-Key' => $api_key,
560 'Content-Type' => 'application/json'
561 ),
562 'body' => json_encode($fetch_data),
563 'timeout' => 30
564 ));
565
566 if (is_wp_error($response)) {
567 return array();
568 }
569
570 $response_code = wp_remote_retrieve_response_code($response);
571
572 if ($response_code !== 200) {
573 return array();
574 }
575
576 $body = wp_remote_retrieve_body($response);
577 $data = json_decode($body, true);
578
579 if (!isset($data['vectors'])) {
580 return array();
581 }
582
583 $processed_data = array();
584
585 foreach ($data['vectors'] as $vector_id => $vector_data) {
586 $metadata = $vector_data['metadata'] ?? array();
587 $source_url = $metadata['source_url'] ?? '';
588
589 if (!empty($source_url)) {
590 $post_id = url_to_postid($source_url);
591 if ($post_id) {
592 $created_at = $metadata['created_at'] ?? '';
593 $processed_date = 'Recently'; // Default
594
595 if (!empty($created_at)) {
596 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
597 if ($timestamp) {
598 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
599 }
600 }
601
602 $processed_data[$post_id] = array(
603 'db_id' => $vector_id,
604 'processed_date' => $processed_date,
605 'url' => $source_url,
606 'source' => 'pinecone',
607 'timestamp' => $timestamp ?? current_time('timestamp')
608 );
609 }
610 }
611 }
612
613 return $processed_data;
614
615 } catch (Exception $e) {
616 return array();
617 }
618 }
619 // ========================================
620 // PINECONE DELETE OPERATIONS
621 // ========================================
622
623 /**
624 * Delete all vectors from Pinecone
625 */
626 public function mxchat_delete_all_from_pinecone($pinecone_options) {
627 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
628 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
629
630 if (empty($api_key) || empty($host)) {
631 return array(
632 'success' => false,
633 'message' => 'Missing Pinecone API credentials'
634 );
635 }
636
637 try {
638 // First, get all vector IDs by scanning Pinecone directly
639 $all_vector_ids = array();
640
641 // Get fresh data from Pinecone
642 $records = $this->mxchat_get_recent_1k_entries($pinecone_options);
643 foreach ($records as $record) {
644 if (!empty($record->id)) {
645 $all_vector_ids[] = $record->id;
646 }
647 }
648
649 if (empty($all_vector_ids)) {
650 return array(
651 'success' => true,
652 'message' => 'No vectors found to delete'
653 );
654 }
655
656 // Delete vectors in batches (Pinecone has limits on batch operations)
657 $batch_size = 100;
658 $batches = array_chunk($all_vector_ids, $batch_size);
659 $deleted_count = 0;
660 $failed_batches = 0;
661
662 foreach ($batches as $batch) {
663 $result = $this->mxchat_delete_pinecone_batch($batch, $api_key, $host);
664 if ($result['success']) {
665 $deleted_count += count($batch);
666 } else {
667 $failed_batches++;
668 }
669 }
670
671 if ($failed_batches > 0) {
672 return array(
673 'success' => false,
674 'message' => sprintf('Deleted %d vectors, but %d batches failed', $deleted_count, $failed_batches)
675 );
676 }
677
678 return array(
679 'success' => true,
680 'message' => "Successfully deleted {$deleted_count} vectors from Pinecone"
681 );
682
683 } catch (Exception $e) {
684 return array(
685 'success' => false,
686 'message' => $e->getMessage()
687 );
688 }
689 }
690
691 /**
692 * Deletes batch of vectors from Pinecone database
693 */
694 private function mxchat_delete_pinecone_batch($vector_ids, $api_key, $host) {
695 // Build the API endpoint
696 $api_endpoint = "https://{$host}/vectors/delete";
697
698 // Prepare the request body with the IDs
699 $request_body = array(
700 'ids' => $vector_ids
701 );
702
703 // Make the deletion request
704 $response = wp_remote_post($api_endpoint, array(
705 'headers' => array(
706 'Api-Key' => $api_key,
707 'accept' => 'application/json',
708 'content-type' => 'application/json'
709 ),
710 'body' => wp_json_encode($request_body),
711 'timeout' => 60, // Increased timeout for batch operations
712 'method' => 'POST'
713 ));
714
715 // Handle WordPress HTTP API errors
716 if (is_wp_error($response)) {
717 return array(
718 'success' => false,
719 'message' => $response->get_error_message()
720 );
721 }
722
723 // Check response status
724 $response_code = wp_remote_retrieve_response_code($response);
725 $response_body = wp_remote_retrieve_body($response);
726
727 // Pinecone returns 200 for successful deletion
728 if ($response_code !== 200) {
729 //error_log('Pinecone batch deletion failed: HTTP ' . $response_code . ' - ' . $response_body);
730 return array(
731 'success' => false,
732 'message' => sprintf(
733 'Pinecone API error (HTTP %d): %s',
734 $response_code,
735 $response_body
736 )
737 );
738 }
739
740 return array(
741 'success' => true,
742 'message' => 'Batch deleted successfully from Pinecone'
743 );
744 }
745
746
747 /**
748 * Deletes vector from Pinecone using API request
749 */
750 public function mxchat_delete_from_pinecone_by_vector_id($vector_id, $api_key, $host, $namespace = '') {
751 //error_log('=== PINECONE DELETE OPERATION ===');
752 //error_log('Vector ID: ' . $vector_id);
753 //error_log('Host: ' . $host);
754 //error_log('API Key: ' . (empty($api_key) ? 'EMPTY' : 'SET'));
755
756 // First, let's verify the vector exists before trying to delete
757 $fetch_url = "https://{$host}/vectors/fetch";
758
759 $fetch_params = array(
760 'ids' => array($vector_id)
761 );
762
763 // Add namespace if provided (though you said you're not using namespaces)
764 if (!empty($namespace)) {
765 $fetch_params['namespace'] = $namespace;
766 }
767
768 // Construct URL with query parameters for GET request
769 $fetch_url_with_params = $fetch_url . '?' . http_build_query($fetch_params);
770
771 $fetch_response = wp_remote_get($fetch_url_with_params, array(
772 'headers' => array(
773 'Api-Key' => $api_key,
774 'accept' => 'application/json'
775 ),
776 'timeout' => 15
777 ));
778
779 if (!is_wp_error($fetch_response) && wp_remote_retrieve_response_code($fetch_response) === 200) {
780 $fetch_body = wp_remote_retrieve_body($fetch_response);
781 $fetch_data = json_decode($fetch_body, true);
782
783 //error_log('DEBUG: Fetch response: ' . print_r($fetch_data, true));
784
785 if (isset($fetch_data['vectors']) && isset($fetch_data['vectors'][$vector_id])) {
786 //error_log('DEBUG: Vector EXISTS in this index before deletion');
787 } else {
788 //error_log('WARNING: Vector NOT FOUND in this index! It may be in a different bot\'s index');
789 // You might want to return an error here
790 }
791 } else {
792 //error_log('DEBUG: Could not fetch vector to verify existence');
793 }
794
795 // Now proceed with deletion
796 $api_endpoint = "https://{$host}/vectors/delete";
797
798 // Prepare the request body with the ID
799 $request_body = array(
800 'ids' => array($vector_id)
801 );
802
803 // Add namespace if provided
804 if (!empty($namespace)) {
805 $request_body['namespace'] = $namespace;
806 }
807
808 //error_log('DEBUG: Delete request body: ' . json_encode($request_body));
809 //error_log('DEBUG: Delete endpoint: ' . $api_endpoint);
810
811 // Make the deletion request
812 $response = wp_remote_post($api_endpoint, array(
813 'headers' => array(
814 'Api-Key' => $api_key,
815 'accept' => 'application/json',
816 'content-type' => 'application/json'
817 ),
818 'body' => wp_json_encode($request_body),
819 'timeout' => 30
820 ));
821
822 // Handle WordPress HTTP API errors
823 if (is_wp_error($response)) {
824 //error_log('DEBUG: WP Error: ' . $response->get_error_message());
825 return array(
826 'success' => false,
827 'message' => $response->get_error_message()
828 );
829 }
830
831 // Check response status
832 $response_code = wp_remote_retrieve_response_code($response);
833 $response_body = wp_remote_retrieve_body($response);
834
835 //error_log('DEBUG: Delete response code: ' . $response_code);
836 //error_log('DEBUG: Delete response body: ' . $response_body);
837
838 // Pinecone returns 200 for successful deletion (even if vector didn't exist)
839 if ($response_code !== 200) {
840 //error_log('DEBUG: Non-200 response from Pinecone');
841 return array(
842 'success' => false,
843 'message' => sprintf(
844 'Pinecone API error (HTTP %d): %s',
845 $response_code,
846 $response_body
847 )
848 );
849 }
850
851 // After deletion, verify it's actually gone
852 sleep(1); // Give Pinecone a moment to process
853
854 $verify_response = wp_remote_get($fetch_url_with_params, array(
855 'headers' => array(
856 'Api-Key' => $api_key,
857 'accept' => 'application/json'
858 ),
859 'timeout' => 15
860 ));
861
862 if (!is_wp_error($verify_response) && wp_remote_retrieve_response_code($verify_response) === 200) {
863 $verify_body = wp_remote_retrieve_body($verify_response);
864 $verify_data = json_decode($verify_body, true);
865
866 if (isset($verify_data['vectors']) && isset($verify_data['vectors'][$vector_id])) {
867 //error_log('ERROR: Vector STILL EXISTS after deletion attempt!');
868 return array(
869 'success' => false,
870 'message' => 'Vector still exists after deletion attempt'
871 );
872 } else {
873 //error_log('SUCCESS: Vector confirmed deleted (or never existed)');
874 }
875 }
876
877 //error_log('=== END PINECONE DELETE OPERATION ===');
878
879 return array(
880 'success' => true,
881 'message' => 'Vector deleted successfully from Pinecone'
882 );
883 }
884
885 /**
886 * Deletes data from Pinecone using a source URL
887 */
888 public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
889 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
890 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
891
892 if (empty($host) || empty($api_key)) {
893 //error_log('MXChat: Pinecone deletion failed - missing configuration');
894 return false;
895 }
896
897 $api_endpoint = "https://{$host}/vectors/delete";
898 $vector_id = md5($source_url);
899
900 $request_body = array(
901 'ids' => array($vector_id)
902 );
903
904 $response = wp_remote_post($api_endpoint, array(
905 'headers' => array(
906 'Api-Key' => $api_key,
907 'accept' => 'application/json',
908 'content-type' => 'application/json'
909 ),
910 'body' => wp_json_encode($request_body),
911 'timeout' => 30
912 ));
913
914 if (is_wp_error($response)) {
915 //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
916 return false;
917 }
918
919 $response_code = wp_remote_retrieve_response_code($response);
920 if ($response_code !== 200) {
921 //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
922 return false;
923 }
924
925 return true;
926 }
927
928
929 /**
930 * Deletes data from Pinecone index using API key
931 */
932 private function mxchat_delete_from_pinecone($urls, $api_key, $environment, $index_name) {
933 // Get the Pinecone host from options (matching your store_in_pinecone_main pattern)
934 $options = get_option('mxchat_pinecone_addon_options');
935 $host = $options['mxchat_pinecone_host'] ?? '';
936
937 if (empty($host)) {
938 return array(
939 'success' => false,
940 'message' => 'Pinecone host is not configured. Please set the host in your settings.'
941 );
942 }
943
944 // Build API endpoint using the configured host
945 $api_endpoint = "https://{$host}/vectors/delete";
946
947 // Create vector IDs from URLs (matching your store method's ID generation)
948 $vector_ids = array_map('md5', $urls);
949
950 // Prepare the delete request body
951 $request_body = array(
952 'ids' => $vector_ids,
953 'filter' => array(
954 'source_url' => array(
955 '$in' => $urls
956 )
957 )
958 );
959
960 // Make the deletion request
961 $response = wp_remote_post($api_endpoint, array(
962 'headers' => array(
963 'Api-Key' => $api_key,
964 'accept' => 'application/json',
965 'content-type' => 'application/json'
966 ),
967 'body' => wp_json_encode($request_body),
968 'timeout' => 30,
969 'data_format' => 'body'
970 ));
971
972 // Handle WordPress HTTP API errors
973 if (is_wp_error($response)) {
974 return array(
975 'success' => false,
976 'message' => $response->get_error_message()
977 );
978 }
979
980 // Check response status
981 $response_code = wp_remote_retrieve_response_code($response);
982 if ($response_code !== 200) {
983 $body = wp_remote_retrieve_body($response);
984 return array(
985 'success' => false,
986 'message' => sprintf(
987 'Pinecone API error (HTTP %d): %s',
988 $response_code,
989 $body
990 )
991 );
992 }
993
994 // Parse response body
995 $body = wp_remote_retrieve_body($response);
996 $response_data = json_decode($body, true);
997
998 // Final validation of the response
999 if (json_last_error() !== JSON_ERROR_NONE) {
1000 return array(
1001 'success' => false,
1002 'message' => 'Failed to parse Pinecone response: ' . json_last_error_msg()
1003 );
1004 }
1005
1006 return array(
1007 'success' => true,
1008 'message' => sprintf('Successfully deleted %d vectors from Pinecone', count($vector_ids))
1009 );
1010 }
1011
1012
1013
1014 /**
1015 * Retrieves processed content from Pinecone API
1016 */
1017 public function mxchat_get_pinecone_processed_content($pinecone_options) {
1018 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1019 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1020
1021 if (empty($api_key) || empty($host)) {
1022 return array();
1023 }
1024
1025 $pinecone_data = array();
1026
1027 try {
1028 // Always get fresh data from Pinecone
1029 $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
1030
1031 // Method 2: Final fallback - try stats endpoint (if available)
1032 if (empty($pinecone_data)) {
1033 $stats_url = "https://{$host}/describe_index_stats";
1034
1035 $response = wp_remote_post($stats_url, array(
1036 'headers' => array(
1037 'Api-Key' => $api_key,
1038 'Content-Type' => 'application/json'
1039 ),
1040 'body' => json_encode(array()),
1041 'timeout' => 30
1042 ));
1043
1044 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1045 $body = wp_remote_retrieve_body($response);
1046 $stats_data = json_decode($body, true);
1047 }
1048 }
1049
1050 } catch (Exception $e) {
1051 // Log error but don't return cached data
1052 }
1053
1054 return $pinecone_data;
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