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

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