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

1,335 lines 50.8 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 //error_log('DEBUG: Missing required Pinecone parameters');
43 return array('data' => array(), 'total' => 0, 'total_in_database' => 0, 'showing_recent_only' => false);
44 }
45
46 try {
47 // Get total count for the banner message (bot-specific)
48 $total_in_database = $this->mxchat_get_pinecone_total_count($pinecone_options, $bot_id);
49
50 // Check cache first for consistency (bot-specific cache)
51 $cache_key = 'mxchat_pinecone_recent_1k_cache_' . $bot_id;
52 $all_records = get_transient($cache_key);
53
54 if ($all_records === false) {
55 // Cache miss - get fresh data (bot-specific)
56 $all_records = $this->mxchat_get_recent_1k_entries($pinecone_options, $bot_id);
57 }
58
59 // Filter by search query if provided
60 if (!empty($search_query)) {
61 $all_records = array_filter($all_records, function($record) use ($search_query) {
62 $content = $record->article_content ?? '';
63 $source_url = $record->source_url ?? '';
64 return stripos($content, $search_query) !== false || stripos($source_url, $search_query) !== false;
65 });
66 }
67
68 // Handle pagination
69 $total = count($all_records);
70 $offset = ($page - 1) * $per_page;
71 $paged_records = array_slice($all_records, $offset, $per_page);
72
73 //error_log('DEBUG: Returning ' . count($paged_records) . ' records for bot ' . $bot_id);
74
75 return array(
76 'data' => $paged_records,
77 'total' => $total,
78 'total_in_database' => $total_in_database,
79 'showing_recent_only' => ($total_in_database > 1000)
80 );
81
82 } catch (Exception $e) {
83 //error_log('DEBUG: Exception: ' . $e->getMessage());
84 return array('data' => array(), 'total' => 0, 'total_in_database' => 0, 'showing_recent_only' => false);
85 }
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 private function mxchat_get_recent_1k_entries($pinecone_options, $bot_id = 'default') {
146 global $wpdb;
147 error_log('=== DEBUG: mxchat_get_recent_1k_entries started ===');
148 error_log('DEBUG: Bot ID: ' . $bot_id);
149
150 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
151 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
152 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
153
154 if (empty($api_key) || empty($host)) {
155 return array();
156 }
157
158 try {
159 $all_records = array();
160 $seen_ids = array();
161 $query_url = "https://{$host}/query";
162
163 $fixed_vectors = $this->mxchat_generate_fixed_query_vectors();
164
165 foreach ($fixed_vectors as $vector_index => $query_vector) {
166 error_log('DEBUG: Using fixed query vector ' . ($vector_index + 1) . '/' . count($fixed_vectors));
167
168 $query_data = array(
169 'includeMetadata' => true,
170 'includeValues' => false,
171 'topK' => 3000,
172 'vector' => $query_vector
173 );
174
175 // Add namespace if provided
176 if (!empty($namespace)) {
177 $query_data['namespace'] = $namespace;
178 }
179
180 // REMOVE THE BOT_ID FILTER - Each bot has its own Pinecone index
181 // No need to filter by bot_id within the index
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 // Remove the bot_id checking since each bot has its own index
203
204 // Get created_at timestamp
205 $created_at = $metadata['created_at'] ??
206 $metadata['last_updated'] ??
207 $metadata['timestamp'] ??
208 time();
209
210 if (!is_numeric($created_at)) {
211 $created_at = strtotime($created_at) ?: time();
212 }
213
214 $all_records[] = (object) array(
215 'id' => $match_id,
216 'article_content' => $metadata['text'] ?? '',
217 'source_url' => $metadata['source_url'] ?? '',
218 'role_restriction' => $metadata['role_restriction'] ?? 'public',
219 'bot_id' => $bot_id, // Just set it to current bot for display
220 'created_at' => $created_at,
221 'data_source' => 'pinecone'
222 );
223
224 $seen_ids[$match_id] = true;
225 }
226 }
227 }
228 }
229
230 usleep(200000); // 0.2 second delay
231 }
232
233 // Sort by created_at (newest first) and take top 1K
234 usort($all_records, function($a, $b) {
235 return $b->created_at - $a->created_at;
236 });
237
238 $recent_1k = array_slice($all_records, 0, 1000);
239
240 // Check for role restrictions stored in WordPress
241 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
242 foreach ($recent_1k as &$record) {
243 if (empty($record->role_restriction) || $record->role_restriction === 'public') {
244 $stored_role = $wpdb->get_var($wpdb->prepare(
245 "SELECT role_restriction FROM {$roles_table} WHERE vector_id = %s AND bot_id = %s",
246 $record->id,
247 $bot_id
248 ));
249
250 if ($stored_role) {
251 $record->role_restriction = $stored_role;
252 }
253 }
254 }
255
256 error_log('DEBUG: Found ' . count($all_records) . ' total unique records, returning top ' . count($recent_1k));
257
258 // Cache the results
259 $cache_key = 'mxchat_pinecone_recent_1k_cache_' . $bot_id;
260 set_transient($cache_key, $recent_1k, 60);
261
262 return $recent_1k;
263
264 } catch (Exception $e) {
265 error_log('DEBUG: Exception in get_recent_1k_entries: ' . $e->getMessage());
266 return array();
267 }
268 }
269
270 /**
271 * Generate fixed query vectors for consistent results
272 */
273 private function mxchat_generate_fixed_query_vectors() {
274 $dimensions = $this->mxchat_get_embedding_dimensions();
275 $vectors = array();
276
277 // Create 5 fixed vectors with different patterns for better coverage
278 $patterns = array(
279 'zeros_with_ones' => 0.1, // Mostly zeros with some 1s
280 'ascending' => 0.2, // Ascending pattern
281 'descending' => 0.3, // Descending pattern
282 'alternating' => 0.4, // Alternating positive/negative
283 'center_weighted' => 0.5 // Higher values in center
284 );
285
286 foreach ($patterns as $pattern_name => $seed) {
287 $vector = array();
288
289 for ($i = 0; $i < $dimensions; $i++) {
290 switch ($pattern_name) {
291 case 'zeros_with_ones':
292 $vector[] = ($i % 10 === 0) ? 1.0 : 0.0;
293 break;
294 case 'ascending':
295 $vector[] = ($i / $dimensions) * 2 - 1; // Range -1 to 1
296 break;
297 case 'descending':
298 $vector[] = (($dimensions - $i) / $dimensions) * 2 - 1;
299 break;
300 case 'alternating':
301 $vector[] = ($i % 2 === 0) ? $seed : -$seed;
302 break;
303 case 'center_weighted':
304 $center = $dimensions / 2;
305 $distance = abs($i - $center) / $center;
306 $vector[] = (1 - $distance) * $seed;
307 break;
308 }
309 }
310
311 // Normalize the vector to unit length
312 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $vector)));
313 if ($magnitude > 0) {
314 $vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $vector);
315 }
316
317 $vectors[] = $vector;
318 }
319
320 return $vectors;
321 }
322
323
324 /**
325 * Scan Pinecone for processed content (MISSING FUNCTION - ADD THIS)
326 */
327 public function mxchat_scan_pinecone_for_processed_content($pinecone_options) {
328 //error_log('=== DEBUG: Starting mxchat_scan_pinecone_for_processed_content ===');
329
330 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
331 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
332
333 if (empty($api_key) || empty($host)) {
334 //error_log('DEBUG: Missing API credentials for scanning');
335 return array();
336 }
337
338 try {
339 // Use multiple random vectors to get better coverage
340 $all_matches = array();
341 $seen_ids = array();
342
343 // Try 3 different random vectors to get better coverage
344 for ($i = 0; $i < 3; $i++) {
345 //error_log('DEBUG: Scanning attempt ' . ($i + 1) . '/3');
346
347 $query_url = "https://{$host}/query";
348
349 // Generate random vector with CORRECT dimensions
350 $random_vector = $this->mxchat_generate_random_vector();
351
352 $query_data = array(
353 'includeMetadata' => true,
354 'includeValues' => false,
355 'topK' => 10000,
356 'vector' => $random_vector
357 );
358
359 $response = wp_remote_post($query_url, array(
360 'headers' => array(
361 'Api-Key' => $api_key,
362 'Content-Type' => 'application/json'
363 ),
364 'body' => json_encode($query_data),
365 'timeout' => 30
366 ));
367
368 if (is_wp_error($response)) {
369 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' WP error: ' . $response->get_error_message());
370 continue;
371 }
372
373 $response_code = wp_remote_retrieve_response_code($response);
374 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' response code: ' . $response_code);
375
376 if ($response_code !== 200) {
377 $error_body = wp_remote_retrieve_body($response);
378 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' failed with body: ' . substr($error_body, 0, 500));
379 continue;
380 }
381
382 $body = wp_remote_retrieve_body($response);
383 $data = json_decode($body, true);
384
385 if (isset($data['matches'])) {
386 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' returned ' . count($data['matches']) . ' matches');
387 foreach ($data['matches'] as $match) {
388 $match_id = $match['id'] ?? '';
389 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
390 $all_matches[] = $match;
391 $seen_ids[$match_id] = true;
392 }
393 }
394 } else {
395 //error_log('DEBUG: Query attempt ' . ($i + 1) . ' - no matches key in response');
396 }
397 }
398
399 //error_log('DEBUG: Total unique matches found: ' . count($all_matches));
400
401 // Convert matches to processed data format
402 $processed_data = array();
403 $vector_ids_for_cache = array();
404
405 foreach ($all_matches as $match) {
406 $metadata = $match['metadata'] ?? array();
407 $source_url = $metadata['source_url'] ?? '';
408 $match_id = $match['id'] ?? '';
409
410 if (!empty($source_url) && !empty($match_id)) {
411 $post_id = url_to_postid($source_url);
412 if ($post_id) {
413 $created_at = $metadata['created_at'] ?? '';
414 $processed_date = 'Recently';
415
416 if (!empty($created_at)) {
417 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
418 if ($timestamp) {
419 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
420 }
421 }
422
423 $processed_data[$post_id] = array(
424 'db_id' => $match_id,
425 'processed_date' => $processed_date,
426 'url' => $source_url,
427 'source' => 'pinecone',
428 'timestamp' => $timestamp ?? current_time('timestamp')
429 );
430
431 $vector_ids_for_cache[] = $match_id;
432 }
433 }
434 }
435
436 // Update the vector IDs cache for future use
437 if (!empty($vector_ids_for_cache)) {
438 update_option('mxchat_pinecone_vector_ids_cache', $vector_ids_for_cache);
439 //error_log('DEBUG: Updated vector IDs cache with ' . count($vector_ids_for_cache) . ' IDs');
440 }
441
442 //error_log('DEBUG: Returning ' . count($processed_data) . ' processed items from scanning');
443 return $processed_data;
444
445 } catch (Exception $e) {
446 //error_log('DEBUG: Exception in scan_pinecone_for_processed_content: ' . $e->getMessage());
447 return array();
448 }
449 }
450
451 private function mxchat_get_pinecone_total_count($pinecone_options, $bot_id = 'default') {
452 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
453 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
454 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
455
456 if (empty($api_key) || empty($host)) {
457 return 0;
458 }
459
460 try {
461 $stats_url = "https://{$host}/describe_index_stats";
462
463 // Try GET request first
464 $response = wp_remote_get($stats_url, array(
465 'headers' => array(
466 'Api-Key' => $api_key,
467 'Accept' => 'application/json'
468 ),
469 'timeout' => 15
470 ));
471
472 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
473 $body = wp_remote_retrieve_body($response);
474 $stats_data = json_decode($body, true);
475
476 $total_count = $stats_data['totalVectorCount'] ?? 0;
477 if ($total_count > 0) {
478 error_log('DEBUG: Got total count from stats API: ' . $total_count);
479 return intval($total_count);
480 }
481 }
482
483 // If GET fails, log the error for debugging
484 if (!is_wp_error($response)) {
485 error_log('DEBUG: Stats API failed with response code: ' . wp_remote_retrieve_response_code($response));
486 error_log('DEBUG: Stats API response body: ' . wp_remote_retrieve_body($response));
487 }
488
489 } catch (Exception $e) {
490 error_log('DEBUG: Exception getting total count: ' . $e->getMessage());
491 }
492
493 return 0;
494 }
495 /**
496 * Clear all caches for a specific bot (and general caches)
497 */
498 public function mxchat_clear_bot_caches($bot_id = 'default') {
499 //error_log('DEBUG: Clearing all caches for bot: ' . $bot_id);
500
501 // Clear bot-specific caches
502 delete_transient('mxchat_pinecone_recent_1k_cache_' . $bot_id);
503 delete_transient('mxchat_pinecone_recent_1k_' . $bot_id);
504 delete_transient('mxchat_pinecone_total_count_' . $bot_id);
505 delete_option('mxchat_pinecone_vector_ids_cache_' . $bot_id);
506 delete_option('mxchat_pinecone_processed_cache_' . $bot_id);
507 delete_option('mxchat_processed_content_cache_' . $bot_id);
508
509 // Also clear general caches for backward compatibility
510 delete_transient('mxchat_pinecone_recent_1k_cache');
511 delete_transient('mxchat_pinecone_recent_1k');
512 delete_transient('mxchat_pinecone_total_count');
513 delete_option('mxchat_pinecone_vector_ids_cache');
514 delete_option('mxchat_pinecone_processed_cache');
515 delete_option('mxchat_processed_content_cache');
516
517 //error_log('DEBUG: All caches cleared for bot: ' . $bot_id);
518
519 return true;
520 }
521
522 /**
523 * Clear caches for all bots
524 */
525 public function mxchat_clear_all_bot_caches() {
526 // Clear default/general caches
527 $this->mxchat_clear_bot_caches('default');
528
529 // If multi-bot addon is active, clear caches for all bots
530 if (class_exists('MxChat_Multi_Bot_Manager')) {
531 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
532 $available_bots = $multi_bot_manager->get_available_bots();
533
534 foreach ($available_bots as $bot_id => $bot_name) {
535 if ($bot_id !== 'default') {
536 $this->mxchat_clear_bot_caches($bot_id);
537 }
538 }
539 }
540
541 return true;
542 }
543
544 /**
545 * Call this after adding new content to refresh the view for specific bot
546 */
547 public function mxchat_refresh_after_new_content($pinecone_options, $bot_id = 'default') {
548 //error_log('DEBUG: Refreshing after new content added for bot: ' . $bot_id);
549
550 // Use the new comprehensive cache clearing method
551 $this->mxchat_clear_bot_caches($bot_id);
552
553 // Force fresh fetch on next page load
554 return true;
555 }
556
557 /**
558 * Get bot-specific Pinecone configuration for database operations
559 */
560 public function mxchat_get_bot_pinecone_options($bot_id = 'default') {
561 error_log('DEBUG: Getting Pinecone options for bot: ' . $bot_id);
562
563 // If default bot or multi-bot add-on not active, use default Pinecone config
564 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
565 $addon_options = get_option('mxchat_pinecone_addon_options', array());
566 error_log('DEBUG: Using default Pinecone options');
567 return $addon_options;
568 }
569
570 // Get bot-specific configuration using the filter
571 $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
572
573 error_log('DEBUG: Bot config from filter: ' . print_r($bot_config, true));
574
575 // Check if we got valid bot-specific config
576 if (!empty($bot_config) && isset($bot_config['use_pinecone']) && $bot_config['use_pinecone']) {
577 // Convert bot config to the format expected by fetch functions
578 $pinecone_options = array(
579 'mxchat_use_pinecone' => '1',
580 'mxchat_pinecone_api_key' => $bot_config['api_key'] ?? '',
581 'mxchat_pinecone_host' => $bot_config['host'] ?? '',
582 'mxchat_pinecone_namespace' => $bot_config['namespace'] ?? '',
583 'mxchat_pinecone_environment' => '',
584 'mxchat_pinecone_index' => ''
585 );
586
587 error_log('DEBUG: Returning bot-specific Pinecone options for bot: ' . $bot_id);
588 return $pinecone_options;
589 }
590
591 // Fallback to default options if bot-specific config is invalid
592 error_log('DEBUG: Bot-specific config invalid, falling back to default');
593 return get_option('mxchat_pinecone_addon_options', array());
594 }
595
596 /**
597 * Get bot-specific Pinecone configuration
598 * Used in the knowledge retrieval functions
599 */
600 private function get_bot_pinecone_config($bot_id = 'default') {
601 error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
602
603 // If default bot or multi-bot add-on not active, use default Pinecone config
604 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
605 error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
606 $addon_options = get_option('mxchat_pinecone_addon_options', array());
607 $config = array(
608 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
609 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
610 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
611 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
612 );
613 error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
614 return $config;
615 }
616
617 error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
618
619 // Hook for multi-bot add-on to provide bot-specific Pinecone config
620 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
621
622 if (!empty($bot_pinecone_config)) {
623 error_log("MXCHAT DEBUG: Got bot-specific config from filter");
624 error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
625 error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
626 error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
627 } else {
628 error_log("MXCHAT DEBUG: Filter returned empty config!");
629 }
630
631 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
632 }
633 /**
634 * Fetches vectors from Pinecone using provided IDs (for content selection feature)
635 */
636 public function fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
637 //error_log('=== DEBUG: fetch_pinecone_vectors_by_ids started (content selection method) ===');
638
639 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
640 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
641
642 //error_log('DEBUG: API key present: ' . (!empty($api_key) ? 'YES' : 'NO'));
643 //error_log('DEBUG: Host: ' . $host);
644 //error_log('DEBUG: Vector IDs count: ' . count($vector_ids));
645
646 if (empty($api_key) || empty($host) || empty($vector_ids)) {
647 //error_log('DEBUG: Missing parameters for fetch by IDs (content selection)');
648 return array();
649 }
650
651 try {
652 $fetch_url = "https://{$host}/vectors/fetch";
653 //error_log('DEBUG: Fetch URL: ' . $fetch_url);
654
655 // Pinecone fetch API allows fetching specific vectors by ID
656 $fetch_data = array(
657 'ids' => array_values($vector_ids)
658 );
659
660 $response = wp_remote_post($fetch_url, array(
661 'headers' => array(
662 'Api-Key' => $api_key,
663 'Content-Type' => 'application/json'
664 ),
665 'body' => json_encode($fetch_data),
666 'timeout' => 30
667 ));
668
669 if (is_wp_error($response)) {
670 //error_log('DEBUG: Fetch by IDs WP error (content selection): ' . $response->get_error_message());
671 return array();
672 }
673
674 $response_code = wp_remote_retrieve_response_code($response);
675 //error_log('DEBUG: Fetch response code (content selection): ' . $response_code);
676
677 if ($response_code !== 200) {
678 $error_body = wp_remote_retrieve_body($response);
679 //error_log('DEBUG: Fetch failed with body (content selection): ' . $error_body);
680 return array();
681 }
682
683 $body = wp_remote_retrieve_body($response);
684 $data = json_decode($body, true);
685
686 if (!isset($data['vectors'])) {
687 //error_log('DEBUG: No vectors key in response (content selection)');
688 return array();
689 }
690
691 $processed_data = array();
692
693 foreach ($data['vectors'] as $vector_id => $vector_data) {
694 $metadata = $vector_data['metadata'] ?? array();
695 $source_url = $metadata['source_url'] ?? '';
696
697 if (!empty($source_url)) {
698 $post_id = url_to_postid($source_url);
699 if ($post_id) {
700 $created_at = $metadata['created_at'] ?? '';
701 $processed_date = 'Recently'; // Default
702
703 if (!empty($created_at)) {
704 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
705 if ($timestamp) {
706 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
707 }
708 }
709
710 $processed_data[$post_id] = array(
711 'db_id' => $vector_id,
712 'processed_date' => $processed_date,
713 'url' => $source_url,
714 'source' => 'pinecone',
715 'timestamp' => $timestamp ?? current_time('timestamp')
716 );
717 }
718 }
719 }
720
721 //error_log('DEBUG: Processed ' . count($processed_data) . ' records (content selection method)');
722 //error_log('=== DEBUG: fetch_pinecone_vectors_by_ids completed (content selection) ===');
723
724 return $processed_data;
725
726 } catch (Exception $e) {
727 //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids (content selection): ' . $e->getMessage());
728 return array();
729 }
730 }
731
732
733 // ========================================
734 // PINECONE DELETE OPERATIONS
735 // ========================================
736
737 public function mxchat_delete_all_from_pinecone($pinecone_options) {
738 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
739 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
740
741 if (empty($api_key) || empty($host)) {
742 return array(
743 'success' => false,
744 'message' => 'Missing Pinecone API credentials'
745 );
746 }
747
748 try {
749 // First, get all vector IDs
750 $all_vector_ids = array();
751
752 // Try to get from cache first
753 $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
754 if (!empty($cached_vector_ids)) {
755 $all_vector_ids = $cached_vector_ids;
756 } else {
757 // Use the correct method name that exists in your class
758 $records = $this->mxchat_get_recent_1k_entries($pinecone_options);
759 foreach ($records as $record) {
760 if (!empty($record->id)) {
761 $all_vector_ids[] = $record->id;
762 }
763 }
764 }
765
766 if (empty($all_vector_ids)) {
767 return array(
768 'success' => true,
769 'message' => 'No vectors found to delete'
770 );
771 }
772
773 // Delete vectors in batches (Pinecone has limits on batch operations)
774 $batch_size = 100;
775 $batches = array_chunk($all_vector_ids, $batch_size);
776 $deleted_count = 0;
777 $failed_batches = 0;
778
779 foreach ($batches as $batch) {
780 $result = $this->mxchat_delete_pinecone_batch($batch, $api_key, $host);
781 if ($result['success']) {
782 $deleted_count += count($batch);
783 } else {
784 $failed_batches++;
785 //error_log('Failed to delete Pinecone batch: ' . $result['message']);
786 }
787 }
788
789 // CLEAR ALL RELEVANT CACHES - EXACTLY like your single delete
790 delete_transient('mxchat_pinecone_recent_1k_cache');
791 delete_option('mxchat_pinecone_vector_ids_cache');
792 delete_option('mxchat_pinecone_processed_cache');
793 delete_option('mxchat_processed_content_cache');
794
795 // Also force refresh for next page load - EXACTLY like your single delete
796 $this->mxchat_refresh_after_new_content($pinecone_options);
797
798 if ($failed_batches > 0) {
799 return array(
800 'success' => false,
801 'message' => sprintf('Deleted %d vectors, but %d batches failed', $deleted_count, $failed_batches)
802 );
803 }
804
805 return array(
806 'success' => true,
807 'message' => "Successfully deleted {$deleted_count} vectors from Pinecone"
808 );
809
810 } catch (Exception $e) {
811 //error_log('Pinecone delete all exception: ' . $e->getMessage());
812 return array(
813 'success' => false,
814 'message' => $e->getMessage()
815 );
816 }
817 }
818
819
820 /**
821 * Deletes batch of vectors from Pinecone database
822 */
823 private function mxchat_delete_pinecone_batch($vector_ids, $api_key, $host) {
824 // Build the API endpoint
825 $api_endpoint = "https://{$host}/vectors/delete";
826
827 // Prepare the request body with the IDs
828 $request_body = array(
829 'ids' => $vector_ids
830 );
831
832 // Make the deletion request
833 $response = wp_remote_post($api_endpoint, array(
834 'headers' => array(
835 'Api-Key' => $api_key,
836 'accept' => 'application/json',
837 'content-type' => 'application/json'
838 ),
839 'body' => wp_json_encode($request_body),
840 'timeout' => 60, // Increased timeout for batch operations
841 'method' => 'POST'
842 ));
843
844 // Handle WordPress HTTP API errors
845 if (is_wp_error($response)) {
846 return array(
847 'success' => false,
848 'message' => $response->get_error_message()
849 );
850 }
851
852 // Check response status
853 $response_code = wp_remote_retrieve_response_code($response);
854 $response_body = wp_remote_retrieve_body($response);
855
856 // Pinecone returns 200 for successful deletion
857 if ($response_code !== 200) {
858 //error_log('Pinecone batch deletion failed: HTTP ' . $response_code . ' - ' . $response_body);
859 return array(
860 'success' => false,
861 'message' => sprintf(
862 'Pinecone API error (HTTP %d): %s',
863 $response_code,
864 $response_body
865 )
866 );
867 }
868
869 return array(
870 'success' => true,
871 'message' => 'Batch deleted successfully from Pinecone'
872 );
873 }
874
875
876 /**
877 * Deletes vector from Pinecone using API request
878 */
879 public function mxchat_delete_from_pinecone_by_vector_id($vector_id, $api_key, $host, $namespace = '') {
880 error_log('=== PINECONE DELETE OPERATION ===');
881 error_log('Vector ID: ' . $vector_id);
882 error_log('Host: ' . $host);
883 error_log('API Key: ' . (empty($api_key) ? 'EMPTY' : 'SET'));
884
885 // First, let's verify the vector exists before trying to delete
886 $fetch_url = "https://{$host}/vectors/fetch";
887
888 $fetch_params = array(
889 'ids' => array($vector_id)
890 );
891
892 // Add namespace if provided (though you said you're not using namespaces)
893 if (!empty($namespace)) {
894 $fetch_params['namespace'] = $namespace;
895 }
896
897 // Construct URL with query parameters for GET request
898 $fetch_url_with_params = $fetch_url . '?' . http_build_query($fetch_params);
899
900 $fetch_response = wp_remote_get($fetch_url_with_params, array(
901 'headers' => array(
902 'Api-Key' => $api_key,
903 'accept' => 'application/json'
904 ),
905 'timeout' => 15
906 ));
907
908 if (!is_wp_error($fetch_response) && wp_remote_retrieve_response_code($fetch_response) === 200) {
909 $fetch_body = wp_remote_retrieve_body($fetch_response);
910 $fetch_data = json_decode($fetch_body, true);
911
912 error_log('DEBUG: Fetch response: ' . print_r($fetch_data, true));
913
914 if (isset($fetch_data['vectors']) && isset($fetch_data['vectors'][$vector_id])) {
915 error_log('DEBUG: Vector EXISTS in this index before deletion');
916 } else {
917 error_log('WARNING: Vector NOT FOUND in this index! It may be in a different bot\'s index');
918 // You might want to return an error here
919 }
920 } else {
921 error_log('DEBUG: Could not fetch vector to verify existence');
922 }
923
924 // Now proceed with deletion
925 $api_endpoint = "https://{$host}/vectors/delete";
926
927 // Prepare the request body with the ID
928 $request_body = array(
929 'ids' => array($vector_id)
930 );
931
932 // Add namespace if provided
933 if (!empty($namespace)) {
934 $request_body['namespace'] = $namespace;
935 }
936
937 error_log('DEBUG: Delete request body: ' . json_encode($request_body));
938 error_log('DEBUG: Delete endpoint: ' . $api_endpoint);
939
940 // Make the deletion request
941 $response = wp_remote_post($api_endpoint, array(
942 'headers' => array(
943 'Api-Key' => $api_key,
944 'accept' => 'application/json',
945 'content-type' => 'application/json'
946 ),
947 'body' => wp_json_encode($request_body),
948 'timeout' => 30
949 ));
950
951 // Handle WordPress HTTP API errors
952 if (is_wp_error($response)) {
953 error_log('DEBUG: WP Error: ' . $response->get_error_message());
954 return array(
955 'success' => false,
956 'message' => $response->get_error_message()
957 );
958 }
959
960 // Check response status
961 $response_code = wp_remote_retrieve_response_code($response);
962 $response_body = wp_remote_retrieve_body($response);
963
964 error_log('DEBUG: Delete response code: ' . $response_code);
965 error_log('DEBUG: Delete response body: ' . $response_body);
966
967 // Pinecone returns 200 for successful deletion (even if vector didn't exist)
968 if ($response_code !== 200) {
969 error_log('DEBUG: Non-200 response from Pinecone');
970 return array(
971 'success' => false,
972 'message' => sprintf(
973 'Pinecone API error (HTTP %d): %s',
974 $response_code,
975 $response_body
976 )
977 );
978 }
979
980 // After deletion, verify it's actually gone
981 sleep(1); // Give Pinecone a moment to process
982
983 $verify_response = wp_remote_get($fetch_url_with_params, array(
984 'headers' => array(
985 'Api-Key' => $api_key,
986 'accept' => 'application/json'
987 ),
988 'timeout' => 15
989 ));
990
991 if (!is_wp_error($verify_response) && wp_remote_retrieve_response_code($verify_response) === 200) {
992 $verify_body = wp_remote_retrieve_body($verify_response);
993 $verify_data = json_decode($verify_body, true);
994
995 if (isset($verify_data['vectors']) && isset($verify_data['vectors'][$vector_id])) {
996 error_log('ERROR: Vector STILL EXISTS after deletion attempt!');
997 return array(
998 'success' => false,
999 'message' => 'Vector still exists after deletion attempt'
1000 );
1001 } else {
1002 error_log('SUCCESS: Vector confirmed deleted (or never existed)');
1003 }
1004 }
1005
1006 error_log('=== END PINECONE DELETE OPERATION ===');
1007
1008 return array(
1009 'success' => true,
1010 'message' => 'Vector deleted successfully from Pinecone'
1011 );
1012 }
1013
1014 /**
1015 * Deletes data from Pinecone using a source URL
1016 */
1017 public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
1018 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1019 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1020
1021 if (empty($host) || empty($api_key)) {
1022 //error_log('MXChat: Pinecone deletion failed - missing configuration');
1023 return false;
1024 }
1025
1026 $api_endpoint = "https://{$host}/vectors/delete";
1027 $vector_id = md5($source_url);
1028
1029 $request_body = array(
1030 'ids' => array($vector_id)
1031 );
1032
1033 $response = wp_remote_post($api_endpoint, array(
1034 'headers' => array(
1035 'Api-Key' => $api_key,
1036 'accept' => 'application/json',
1037 'content-type' => 'application/json'
1038 ),
1039 'body' => wp_json_encode($request_body),
1040 'timeout' => 30
1041 ));
1042
1043 if (is_wp_error($response)) {
1044 //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
1045 return false;
1046 }
1047
1048 $response_code = wp_remote_retrieve_response_code($response);
1049 if ($response_code !== 200) {
1050 //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
1051 return false;
1052 }
1053
1054 return true;
1055 }
1056
1057
1058 /**
1059 * Deletes data from Pinecone index using API key
1060 */
1061 private function mxchat_delete_from_pinecone($urls, $api_key, $environment, $index_name) {
1062 // Get the Pinecone host from options (matching your store_in_pinecone_main pattern)
1063 $options = get_option('mxchat_pinecone_addon_options');
1064 $host = $options['mxchat_pinecone_host'] ?? '';
1065
1066 if (empty($host)) {
1067 return array(
1068 'success' => false,
1069 'message' => 'Pinecone host is not configured. Please set the host in your settings.'
1070 );
1071 }
1072
1073 // Build API endpoint using the configured host
1074 $api_endpoint = "https://{$host}/vectors/delete";
1075
1076 // Create vector IDs from URLs (matching your store method's ID generation)
1077 $vector_ids = array_map('md5', $urls);
1078
1079 // Prepare the delete request body
1080 $request_body = array(
1081 'ids' => $vector_ids,
1082 'filter' => array(
1083 'source_url' => array(
1084 '$in' => $urls
1085 )
1086 )
1087 );
1088
1089 // Make the deletion request
1090 $response = wp_remote_post($api_endpoint, array(
1091 'headers' => array(
1092 'Api-Key' => $api_key,
1093 'accept' => 'application/json',
1094 'content-type' => 'application/json'
1095 ),
1096 'body' => wp_json_encode($request_body),
1097 'timeout' => 30,
1098 'data_format' => 'body'
1099 ));
1100
1101 // Handle WordPress HTTP API errors
1102 if (is_wp_error($response)) {
1103 return array(
1104 'success' => false,
1105 'message' => $response->get_error_message()
1106 );
1107 }
1108
1109 // Check response status
1110 $response_code = wp_remote_retrieve_response_code($response);
1111 if ($response_code !== 200) {
1112 $body = wp_remote_retrieve_body($response);
1113 return array(
1114 'success' => false,
1115 'message' => sprintf(
1116 'Pinecone API error (HTTP %d): %s',
1117 $response_code,
1118 $body
1119 )
1120 );
1121 }
1122
1123 // Parse response body
1124 $body = wp_remote_retrieve_body($response);
1125 $response_data = json_decode($body, true);
1126
1127 // Final validation of the response
1128 if (json_last_error() !== JSON_ERROR_NONE) {
1129 return array(
1130 'success' => false,
1131 'message' => 'Failed to parse Pinecone response: ' . json_last_error_msg()
1132 );
1133 }
1134
1135 return array(
1136 'success' => true,
1137 'message' => sprintf('Successfully deleted %d vectors from Pinecone', count($vector_ids))
1138 );
1139 }
1140
1141
1142 // ========================================
1143 // VECTOR CACHE MANAGEMENT
1144 // ========================================
1145
1146
1147 /**
1148 * Removes vector ID from cache array option
1149 */
1150 public function mxchat_remove_from_pinecone_vector_cache($vector_id) {
1151 $cached_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
1152 $key = array_search($vector_id, $cached_ids);
1153 if ($key !== false) {
1154 unset($cached_ids[$key]);
1155 update_option('mxchat_pinecone_vector_ids_cache', array_values($cached_ids));
1156 }
1157 }
1158
1159 /**
1160 * Removes vector ID from processed content caches
1161 */
1162 public function mxchat_remove_from_processed_content_caches($vector_id) {
1163 // Get all caches
1164 $pinecone_cache = get_option('mxchat_pinecone_processed_cache', array());
1165 $processed_cache = get_option('mxchat_processed_content_cache', array());
1166
1167 // We need to find the post ID that corresponds to this vector ID
1168 // Vector ID is typically md5 of the source URL
1169 $post_id_to_remove = null;
1170
1171 // Search through caches to find matching post
1172 foreach ($pinecone_cache as $post_id => $cache_data) {
1173 if (isset($cache_data['db_id']) && $cache_data['db_id'] === $vector_id) {
1174 $post_id_to_remove = $post_id;
1175 break;
1176 }
1177 }
1178
1179 // Also check the processed cache
1180 if (!$post_id_to_remove) {
1181 foreach ($processed_cache as $post_id => $cache_data) {
1182 if (isset($cache_data['db_id']) && $cache_data['db_id'] === $vector_id) {
1183 $post_id_to_remove = $post_id;
1184 break;
1185 }
1186 }
1187 }
1188
1189 // If we found the post ID, remove it from both caches
1190 if ($post_id_to_remove) {
1191 unset($pinecone_cache[$post_id_to_remove]);
1192 unset($processed_cache[$post_id_to_remove]);
1193
1194 update_option('mxchat_pinecone_processed_cache', $pinecone_cache);
1195 update_option('mxchat_processed_content_cache', $processed_cache);
1196
1197 //error_log('Removed post ID ' . $post_id_to_remove . ' from processed content caches');
1198 } else {
1199 // If we can't find by vector ID, we might need to reconstruct the URL
1200 // and find the post ID that way
1201 //error_log('Could not find post ID for vector ID: ' . $vector_id);
1202 }
1203 }
1204
1205
1206 /**
1207 * Retrieves and caches Pinecone API processed content
1208 */
1209 public function mxchat_get_pinecone_processed_content($pinecone_options) {
1210 // First check local cache for immediate updates
1211 $cached_data = get_option('mxchat_pinecone_processed_cache', array());
1212
1213 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1214 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1215
1216 if (empty($api_key) || empty($host)) {
1217 // Return only cached data if API credentials are missing
1218 return $cached_data;
1219 }
1220
1221 $pinecone_data = array();
1222
1223 try {
1224 // Method 1: Try to get vectors using cached vector IDs first
1225 $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
1226
1227 if (!empty($cached_vector_ids)) {
1228 $pinecone_data = $this->fetch_pinecone_vectors_by_ids($pinecone_options, $cached_vector_ids);
1229 }
1230
1231 // Method 2: If no cached IDs or fetch failed, use scanning approach
1232 if (empty($pinecone_data)) {
1233 $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
1234 }
1235
1236 // Method 3: Final fallback - try stats endpoint (if available)
1237 if (empty($pinecone_data)) {
1238 $stats_url = "https://{$host}/describe_index_stats";
1239
1240 $response = wp_remote_post($stats_url, array(
1241 'headers' => array(
1242 'Api-Key' => $api_key,
1243 'Content-Type' => 'application/json'
1244 ),
1245 'body' => json_encode(array()),
1246 'timeout' => 30
1247 ));
1248
1249 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1250 $body = wp_remote_retrieve_body($response);
1251 $stats_data = json_decode($body, true);
1252
1253 // Log stats for debugging but don't rely on them for vector listing
1254 //error_log('Pinecone index stats: ' . print_r($stats_data, true));
1255 }
1256 }
1257
1258 } catch (Exception $e) {
1259 //error_log('Pinecone processed content exception: ' . $e->getMessage());
1260 }
1261
1262 // Merge cached data with Pinecone data
1263 // Cache takes priority for recent updates (within last 5 minutes)
1264 $merged_data = $pinecone_data;
1265
1266 foreach ($cached_data as $post_id => $cache_item) {
1267 $cache_timestamp = $cache_item['timestamp'] ?? 0;
1268 $time_diff = current_time('timestamp') - $cache_timestamp;
1269
1270 // If cache item is recent (less than 5 minutes), prioritize it
1271 if ($time_diff < 300) { // 5 minutes = 300 seconds
1272 $merged_data[$post_id] = $cache_item;
1273 } else {
1274 // If not in Pinecone data and cache is old, keep cache but mark as potentially stale
1275 if (!isset($merged_data[$post_id])) {
1276 $merged_data[$post_id] = $cache_item;
1277 }
1278 }
1279 }
1280
1281 return $merged_data;
1282 }
1283
1284
1285 // ========================================
1286 // HELPER METHODS
1287 // ========================================
1288
1289 /**
1290 * Validates Pinecone API credentials
1291 */
1292 private function mxchat_validate_pinecone_credentials($api_key, $host) {
1293 if (empty($api_key) || empty($host)) {
1294 return false;
1295 }
1296 return true;
1297 }
1298
1299 /**
1300 * Get Pinecone API credentials from options
1301 */
1302 private function mxchat_get_pinecone_credentials() {
1303 $options = get_option('mxchat_options', array());
1304 return array(
1305 'api_key' => isset($options['pinecone_api_key']) ? $options['pinecone_api_key'] : '',
1306 'host' => isset($options['pinecone_host']) ? $options['pinecone_host'] : ''
1307 );
1308 }
1309
1310 /**
1311 * Log Pinecone operation errors
1312 */
1313 private function log_pinecone_error($operation, $error_message) {
1314 //error_log("MxChat Pinecone {$operation} Error: " . $error_message);
1315 }
1316
1317 // ========================================
1318 // STATIC ACCESS METHODS (for backward compatibility)
1319 // ========================================
1320
1321 /**
1322 * Get singleton instance
1323 */
1324 public static function get_instance() {
1325 static $instance = null;
1326 if ($instance === null) {
1327 $instance = new self();
1328 }
1329 return $instance;
1330 }
1331 }
1332
1333 // Initialize the Pinecone manager
1334 $mxchat_pinecone_manager = MxChat_Pinecone_Manager::get_instance();
1335