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

1,346 lines 51.0 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 $request_data = array();
464
465 // Add namespace if provided
466 if (!empty($namespace)) {
467 $request_data['namespace'] = $namespace;
468 }
469
470 // REMOVE THE BOT_ID FILTER - Each bot has its own index
471
472 $response = wp_remote_post($stats_url, array(
473 'headers' => array(
474 'Api-Key' => $api_key,
475 'Content-Type' => 'application/json'
476 ),
477 'body' => json_encode($request_data),
478 'timeout' => 15
479 ));
480
481 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
482 $body = wp_remote_retrieve_body($response);
483 $stats_data = json_decode($body, true);
484
485 $total_count = $stats_data['totalVectorCount'] ?? 0;
486 if ($total_count > 0) {
487 error_log('DEBUG: Got total count from stats API: ' . $total_count);
488 return intval($total_count);
489 }
490 }
491
492 // Fallback: estimate from previous scans
493 $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache_' . $bot_id, array());
494 if (!empty($cached_vector_ids)) {
495 $estimated_count = count($cached_vector_ids);
496 return intval($estimated_count);
497 }
498
499 } catch (Exception $e) {
500 error_log('DEBUG: Exception getting total count: ' . $e->getMessage());
501 }
502
503 return 0;
504 }
505
506 /**
507 * Clear all caches for a specific bot (and general caches)
508 */
509 public function mxchat_clear_bot_caches($bot_id = 'default') {
510 //error_log('DEBUG: Clearing all caches for bot: ' . $bot_id);
511
512 // Clear bot-specific caches
513 delete_transient('mxchat_pinecone_recent_1k_cache_' . $bot_id);
514 delete_transient('mxchat_pinecone_recent_1k_' . $bot_id);
515 delete_transient('mxchat_pinecone_total_count_' . $bot_id);
516 delete_option('mxchat_pinecone_vector_ids_cache_' . $bot_id);
517 delete_option('mxchat_pinecone_processed_cache_' . $bot_id);
518 delete_option('mxchat_processed_content_cache_' . $bot_id);
519
520 // Also clear general caches for backward compatibility
521 delete_transient('mxchat_pinecone_recent_1k_cache');
522 delete_transient('mxchat_pinecone_recent_1k');
523 delete_transient('mxchat_pinecone_total_count');
524 delete_option('mxchat_pinecone_vector_ids_cache');
525 delete_option('mxchat_pinecone_processed_cache');
526 delete_option('mxchat_processed_content_cache');
527
528 //error_log('DEBUG: All caches cleared for bot: ' . $bot_id);
529
530 return true;
531 }
532
533 /**
534 * Clear caches for all bots
535 */
536 public function mxchat_clear_all_bot_caches() {
537 // Clear default/general caches
538 $this->mxchat_clear_bot_caches('default');
539
540 // If multi-bot addon is active, clear caches for all bots
541 if (class_exists('MxChat_Multi_Bot_Manager')) {
542 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
543 $available_bots = $multi_bot_manager->get_available_bots();
544
545 foreach ($available_bots as $bot_id => $bot_name) {
546 if ($bot_id !== 'default') {
547 $this->mxchat_clear_bot_caches($bot_id);
548 }
549 }
550 }
551
552 return true;
553 }
554
555 /**
556 * Call this after adding new content to refresh the view for specific bot
557 */
558 public function mxchat_refresh_after_new_content($pinecone_options, $bot_id = 'default') {
559 //error_log('DEBUG: Refreshing after new content added for bot: ' . $bot_id);
560
561 // Use the new comprehensive cache clearing method
562 $this->mxchat_clear_bot_caches($bot_id);
563
564 // Force fresh fetch on next page load
565 return true;
566 }
567
568 /**
569 * Get bot-specific Pinecone configuration for database operations
570 */
571 public function mxchat_get_bot_pinecone_options($bot_id = 'default') {
572 error_log('DEBUG: Getting Pinecone options for bot: ' . $bot_id);
573
574 // If default bot or multi-bot add-on not active, use default Pinecone config
575 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
576 $addon_options = get_option('mxchat_pinecone_addon_options', array());
577 error_log('DEBUG: Using default Pinecone options');
578 return $addon_options;
579 }
580
581 // Get bot-specific configuration using the filter
582 $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
583
584 error_log('DEBUG: Bot config from filter: ' . print_r($bot_config, true));
585
586 // Check if we got valid bot-specific config
587 if (!empty($bot_config) && isset($bot_config['use_pinecone']) && $bot_config['use_pinecone']) {
588 // Convert bot config to the format expected by fetch functions
589 $pinecone_options = array(
590 'mxchat_use_pinecone' => '1',
591 'mxchat_pinecone_api_key' => $bot_config['api_key'] ?? '',
592 'mxchat_pinecone_host' => $bot_config['host'] ?? '',
593 'mxchat_pinecone_namespace' => $bot_config['namespace'] ?? '',
594 'mxchat_pinecone_environment' => '',
595 'mxchat_pinecone_index' => ''
596 );
597
598 error_log('DEBUG: Returning bot-specific Pinecone options for bot: ' . $bot_id);
599 return $pinecone_options;
600 }
601
602 // Fallback to default options if bot-specific config is invalid
603 error_log('DEBUG: Bot-specific config invalid, falling back to default');
604 return get_option('mxchat_pinecone_addon_options', array());
605 }
606
607 /**
608 * Get bot-specific Pinecone configuration
609 * Used in the knowledge retrieval functions
610 */
611 private function get_bot_pinecone_config($bot_id = 'default') {
612 error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
613
614 // If default bot or multi-bot add-on not active, use default Pinecone config
615 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
616 error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
617 $addon_options = get_option('mxchat_pinecone_addon_options', array());
618 $config = array(
619 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
620 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
621 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
622 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
623 );
624 error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
625 return $config;
626 }
627
628 error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
629
630 // Hook for multi-bot add-on to provide bot-specific Pinecone config
631 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
632
633 if (!empty($bot_pinecone_config)) {
634 error_log("MXCHAT DEBUG: Got bot-specific config from filter");
635 error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
636 error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
637 error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
638 } else {
639 error_log("MXCHAT DEBUG: Filter returned empty config!");
640 }
641
642 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
643 }
644 /**
645 * Fetches vectors from Pinecone using provided IDs (for content selection feature)
646 */
647 public function fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
648 //error_log('=== DEBUG: fetch_pinecone_vectors_by_ids started (content selection method) ===');
649
650 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
651 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
652
653 //error_log('DEBUG: API key present: ' . (!empty($api_key) ? 'YES' : 'NO'));
654 //error_log('DEBUG: Host: ' . $host);
655 //error_log('DEBUG: Vector IDs count: ' . count($vector_ids));
656
657 if (empty($api_key) || empty($host) || empty($vector_ids)) {
658 //error_log('DEBUG: Missing parameters for fetch by IDs (content selection)');
659 return array();
660 }
661
662 try {
663 $fetch_url = "https://{$host}/vectors/fetch";
664 //error_log('DEBUG: Fetch URL: ' . $fetch_url);
665
666 // Pinecone fetch API allows fetching specific vectors by ID
667 $fetch_data = array(
668 'ids' => array_values($vector_ids)
669 );
670
671 $response = wp_remote_post($fetch_url, array(
672 'headers' => array(
673 'Api-Key' => $api_key,
674 'Content-Type' => 'application/json'
675 ),
676 'body' => json_encode($fetch_data),
677 'timeout' => 30
678 ));
679
680 if (is_wp_error($response)) {
681 //error_log('DEBUG: Fetch by IDs WP error (content selection): ' . $response->get_error_message());
682 return array();
683 }
684
685 $response_code = wp_remote_retrieve_response_code($response);
686 //error_log('DEBUG: Fetch response code (content selection): ' . $response_code);
687
688 if ($response_code !== 200) {
689 $error_body = wp_remote_retrieve_body($response);
690 //error_log('DEBUG: Fetch failed with body (content selection): ' . $error_body);
691 return array();
692 }
693
694 $body = wp_remote_retrieve_body($response);
695 $data = json_decode($body, true);
696
697 if (!isset($data['vectors'])) {
698 //error_log('DEBUG: No vectors key in response (content selection)');
699 return array();
700 }
701
702 $processed_data = array();
703
704 foreach ($data['vectors'] as $vector_id => $vector_data) {
705 $metadata = $vector_data['metadata'] ?? array();
706 $source_url = $metadata['source_url'] ?? '';
707
708 if (!empty($source_url)) {
709 $post_id = url_to_postid($source_url);
710 if ($post_id) {
711 $created_at = $metadata['created_at'] ?? '';
712 $processed_date = 'Recently'; // Default
713
714 if (!empty($created_at)) {
715 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
716 if ($timestamp) {
717 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
718 }
719 }
720
721 $processed_data[$post_id] = array(
722 'db_id' => $vector_id,
723 'processed_date' => $processed_date,
724 'url' => $source_url,
725 'source' => 'pinecone',
726 'timestamp' => $timestamp ?? current_time('timestamp')
727 );
728 }
729 }
730 }
731
732 //error_log('DEBUG: Processed ' . count($processed_data) . ' records (content selection method)');
733 //error_log('=== DEBUG: fetch_pinecone_vectors_by_ids completed (content selection) ===');
734
735 return $processed_data;
736
737 } catch (Exception $e) {
738 //error_log('DEBUG: Exception in fetch_pinecone_vectors_by_ids (content selection): ' . $e->getMessage());
739 return array();
740 }
741 }
742
743
744 // ========================================
745 // PINECONE DELETE OPERATIONS
746 // ========================================
747
748 public function mxchat_delete_all_from_pinecone($pinecone_options) {
749 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
750 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
751
752 if (empty($api_key) || empty($host)) {
753 return array(
754 'success' => false,
755 'message' => 'Missing Pinecone API credentials'
756 );
757 }
758
759 try {
760 // First, get all vector IDs
761 $all_vector_ids = array();
762
763 // Try to get from cache first
764 $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
765 if (!empty($cached_vector_ids)) {
766 $all_vector_ids = $cached_vector_ids;
767 } else {
768 // Use the correct method name that exists in your class
769 $records = $this->mxchat_get_recent_1k_entries($pinecone_options);
770 foreach ($records as $record) {
771 if (!empty($record->id)) {
772 $all_vector_ids[] = $record->id;
773 }
774 }
775 }
776
777 if (empty($all_vector_ids)) {
778 return array(
779 'success' => true,
780 'message' => 'No vectors found to delete'
781 );
782 }
783
784 // Delete vectors in batches (Pinecone has limits on batch operations)
785 $batch_size = 100;
786 $batches = array_chunk($all_vector_ids, $batch_size);
787 $deleted_count = 0;
788 $failed_batches = 0;
789
790 foreach ($batches as $batch) {
791 $result = $this->mxchat_delete_pinecone_batch($batch, $api_key, $host);
792 if ($result['success']) {
793 $deleted_count += count($batch);
794 } else {
795 $failed_batches++;
796 //error_log('Failed to delete Pinecone batch: ' . $result['message']);
797 }
798 }
799
800 // CLEAR ALL RELEVANT CACHES - EXACTLY like your single delete
801 delete_transient('mxchat_pinecone_recent_1k_cache');
802 delete_option('mxchat_pinecone_vector_ids_cache');
803 delete_option('mxchat_pinecone_processed_cache');
804 delete_option('mxchat_processed_content_cache');
805
806 // Also force refresh for next page load - EXACTLY like your single delete
807 $this->mxchat_refresh_after_new_content($pinecone_options);
808
809 if ($failed_batches > 0) {
810 return array(
811 'success' => false,
812 'message' => sprintf('Deleted %d vectors, but %d batches failed', $deleted_count, $failed_batches)
813 );
814 }
815
816 return array(
817 'success' => true,
818 'message' => "Successfully deleted {$deleted_count} vectors from Pinecone"
819 );
820
821 } catch (Exception $e) {
822 //error_log('Pinecone delete all exception: ' . $e->getMessage());
823 return array(
824 'success' => false,
825 'message' => $e->getMessage()
826 );
827 }
828 }
829
830
831 /**
832 * Deletes batch of vectors from Pinecone database
833 */
834 private function mxchat_delete_pinecone_batch($vector_ids, $api_key, $host) {
835 // Build the API endpoint
836 $api_endpoint = "https://{$host}/vectors/delete";
837
838 // Prepare the request body with the IDs
839 $request_body = array(
840 'ids' => $vector_ids
841 );
842
843 // Make the deletion request
844 $response = wp_remote_post($api_endpoint, array(
845 'headers' => array(
846 'Api-Key' => $api_key,
847 'accept' => 'application/json',
848 'content-type' => 'application/json'
849 ),
850 'body' => wp_json_encode($request_body),
851 'timeout' => 60, // Increased timeout for batch operations
852 'method' => 'POST'
853 ));
854
855 // Handle WordPress HTTP API errors
856 if (is_wp_error($response)) {
857 return array(
858 'success' => false,
859 'message' => $response->get_error_message()
860 );
861 }
862
863 // Check response status
864 $response_code = wp_remote_retrieve_response_code($response);
865 $response_body = wp_remote_retrieve_body($response);
866
867 // Pinecone returns 200 for successful deletion
868 if ($response_code !== 200) {
869 //error_log('Pinecone batch deletion failed: HTTP ' . $response_code . ' - ' . $response_body);
870 return array(
871 'success' => false,
872 'message' => sprintf(
873 'Pinecone API error (HTTP %d): %s',
874 $response_code,
875 $response_body
876 )
877 );
878 }
879
880 return array(
881 'success' => true,
882 'message' => 'Batch deleted successfully from Pinecone'
883 );
884 }
885
886
887 /**
888 * Deletes vector from Pinecone using API request
889 */
890 public function mxchat_delete_from_pinecone_by_vector_id($vector_id, $api_key, $host, $namespace = '') {
891 error_log('=== PINECONE DELETE OPERATION ===');
892 error_log('Vector ID: ' . $vector_id);
893 error_log('Host: ' . $host);
894 error_log('API Key: ' . (empty($api_key) ? 'EMPTY' : 'SET'));
895
896 // First, let's verify the vector exists before trying to delete
897 $fetch_url = "https://{$host}/vectors/fetch";
898
899 $fetch_params = array(
900 'ids' => array($vector_id)
901 );
902
903 // Add namespace if provided (though you said you're not using namespaces)
904 if (!empty($namespace)) {
905 $fetch_params['namespace'] = $namespace;
906 }
907
908 // Construct URL with query parameters for GET request
909 $fetch_url_with_params = $fetch_url . '?' . http_build_query($fetch_params);
910
911 $fetch_response = wp_remote_get($fetch_url_with_params, array(
912 'headers' => array(
913 'Api-Key' => $api_key,
914 'accept' => 'application/json'
915 ),
916 'timeout' => 15
917 ));
918
919 if (!is_wp_error($fetch_response) && wp_remote_retrieve_response_code($fetch_response) === 200) {
920 $fetch_body = wp_remote_retrieve_body($fetch_response);
921 $fetch_data = json_decode($fetch_body, true);
922
923 error_log('DEBUG: Fetch response: ' . print_r($fetch_data, true));
924
925 if (isset($fetch_data['vectors']) && isset($fetch_data['vectors'][$vector_id])) {
926 error_log('DEBUG: Vector EXISTS in this index before deletion');
927 } else {
928 error_log('WARNING: Vector NOT FOUND in this index! It may be in a different bot\'s index');
929 // You might want to return an error here
930 }
931 } else {
932 error_log('DEBUG: Could not fetch vector to verify existence');
933 }
934
935 // Now proceed with deletion
936 $api_endpoint = "https://{$host}/vectors/delete";
937
938 // Prepare the request body with the ID
939 $request_body = array(
940 'ids' => array($vector_id)
941 );
942
943 // Add namespace if provided
944 if (!empty($namespace)) {
945 $request_body['namespace'] = $namespace;
946 }
947
948 error_log('DEBUG: Delete request body: ' . json_encode($request_body));
949 error_log('DEBUG: Delete endpoint: ' . $api_endpoint);
950
951 // Make the deletion request
952 $response = wp_remote_post($api_endpoint, array(
953 'headers' => array(
954 'Api-Key' => $api_key,
955 'accept' => 'application/json',
956 'content-type' => 'application/json'
957 ),
958 'body' => wp_json_encode($request_body),
959 'timeout' => 30
960 ));
961
962 // Handle WordPress HTTP API errors
963 if (is_wp_error($response)) {
964 error_log('DEBUG: WP Error: ' . $response->get_error_message());
965 return array(
966 'success' => false,
967 'message' => $response->get_error_message()
968 );
969 }
970
971 // Check response status
972 $response_code = wp_remote_retrieve_response_code($response);
973 $response_body = wp_remote_retrieve_body($response);
974
975 error_log('DEBUG: Delete response code: ' . $response_code);
976 error_log('DEBUG: Delete response body: ' . $response_body);
977
978 // Pinecone returns 200 for successful deletion (even if vector didn't exist)
979 if ($response_code !== 200) {
980 error_log('DEBUG: Non-200 response from Pinecone');
981 return array(
982 'success' => false,
983 'message' => sprintf(
984 'Pinecone API error (HTTP %d): %s',
985 $response_code,
986 $response_body
987 )
988 );
989 }
990
991 // After deletion, verify it's actually gone
992 sleep(1); // Give Pinecone a moment to process
993
994 $verify_response = wp_remote_get($fetch_url_with_params, array(
995 'headers' => array(
996 'Api-Key' => $api_key,
997 'accept' => 'application/json'
998 ),
999 'timeout' => 15
1000 ));
1001
1002 if (!is_wp_error($verify_response) && wp_remote_retrieve_response_code($verify_response) === 200) {
1003 $verify_body = wp_remote_retrieve_body($verify_response);
1004 $verify_data = json_decode($verify_body, true);
1005
1006 if (isset($verify_data['vectors']) && isset($verify_data['vectors'][$vector_id])) {
1007 error_log('ERROR: Vector STILL EXISTS after deletion attempt!');
1008 return array(
1009 'success' => false,
1010 'message' => 'Vector still exists after deletion attempt'
1011 );
1012 } else {
1013 error_log('SUCCESS: Vector confirmed deleted (or never existed)');
1014 }
1015 }
1016
1017 error_log('=== END PINECONE DELETE OPERATION ===');
1018
1019 return array(
1020 'success' => true,
1021 'message' => 'Vector deleted successfully from Pinecone'
1022 );
1023 }
1024
1025 /**
1026 * Deletes data from Pinecone using a source URL
1027 */
1028 public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
1029 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1030 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1031
1032 if (empty($host) || empty($api_key)) {
1033 //error_log('MXChat: Pinecone deletion failed - missing configuration');
1034 return false;
1035 }
1036
1037 $api_endpoint = "https://{$host}/vectors/delete";
1038 $vector_id = md5($source_url);
1039
1040 $request_body = array(
1041 'ids' => array($vector_id)
1042 );
1043
1044 $response = wp_remote_post($api_endpoint, array(
1045 'headers' => array(
1046 'Api-Key' => $api_key,
1047 'accept' => 'application/json',
1048 'content-type' => 'application/json'
1049 ),
1050 'body' => wp_json_encode($request_body),
1051 'timeout' => 30
1052 ));
1053
1054 if (is_wp_error($response)) {
1055 //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
1056 return false;
1057 }
1058
1059 $response_code = wp_remote_retrieve_response_code($response);
1060 if ($response_code !== 200) {
1061 //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
1062 return false;
1063 }
1064
1065 return true;
1066 }
1067
1068
1069 /**
1070 * Deletes data from Pinecone index using API key
1071 */
1072 private function mxchat_delete_from_pinecone($urls, $api_key, $environment, $index_name) {
1073 // Get the Pinecone host from options (matching your store_in_pinecone_main pattern)
1074 $options = get_option('mxchat_pinecone_addon_options');
1075 $host = $options['mxchat_pinecone_host'] ?? '';
1076
1077 if (empty($host)) {
1078 return array(
1079 'success' => false,
1080 'message' => 'Pinecone host is not configured. Please set the host in your settings.'
1081 );
1082 }
1083
1084 // Build API endpoint using the configured host
1085 $api_endpoint = "https://{$host}/vectors/delete";
1086
1087 // Create vector IDs from URLs (matching your store method's ID generation)
1088 $vector_ids = array_map('md5', $urls);
1089
1090 // Prepare the delete request body
1091 $request_body = array(
1092 'ids' => $vector_ids,
1093 'filter' => array(
1094 'source_url' => array(
1095 '$in' => $urls
1096 )
1097 )
1098 );
1099
1100 // Make the deletion request
1101 $response = wp_remote_post($api_endpoint, array(
1102 'headers' => array(
1103 'Api-Key' => $api_key,
1104 'accept' => 'application/json',
1105 'content-type' => 'application/json'
1106 ),
1107 'body' => wp_json_encode($request_body),
1108 'timeout' => 30,
1109 'data_format' => 'body'
1110 ));
1111
1112 // Handle WordPress HTTP API errors
1113 if (is_wp_error($response)) {
1114 return array(
1115 'success' => false,
1116 'message' => $response->get_error_message()
1117 );
1118 }
1119
1120 // Check response status
1121 $response_code = wp_remote_retrieve_response_code($response);
1122 if ($response_code !== 200) {
1123 $body = wp_remote_retrieve_body($response);
1124 return array(
1125 'success' => false,
1126 'message' => sprintf(
1127 'Pinecone API error (HTTP %d): %s',
1128 $response_code,
1129 $body
1130 )
1131 );
1132 }
1133
1134 // Parse response body
1135 $body = wp_remote_retrieve_body($response);
1136 $response_data = json_decode($body, true);
1137
1138 // Final validation of the response
1139 if (json_last_error() !== JSON_ERROR_NONE) {
1140 return array(
1141 'success' => false,
1142 'message' => 'Failed to parse Pinecone response: ' . json_last_error_msg()
1143 );
1144 }
1145
1146 return array(
1147 'success' => true,
1148 'message' => sprintf('Successfully deleted %d vectors from Pinecone', count($vector_ids))
1149 );
1150 }
1151
1152
1153 // ========================================
1154 // VECTOR CACHE MANAGEMENT
1155 // ========================================
1156
1157
1158 /**
1159 * Removes vector ID from cache array option
1160 */
1161 public function mxchat_remove_from_pinecone_vector_cache($vector_id) {
1162 $cached_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
1163 $key = array_search($vector_id, $cached_ids);
1164 if ($key !== false) {
1165 unset($cached_ids[$key]);
1166 update_option('mxchat_pinecone_vector_ids_cache', array_values($cached_ids));
1167 }
1168 }
1169
1170 /**
1171 * Removes vector ID from processed content caches
1172 */
1173 public function mxchat_remove_from_processed_content_caches($vector_id) {
1174 // Get all caches
1175 $pinecone_cache = get_option('mxchat_pinecone_processed_cache', array());
1176 $processed_cache = get_option('mxchat_processed_content_cache', array());
1177
1178 // We need to find the post ID that corresponds to this vector ID
1179 // Vector ID is typically md5 of the source URL
1180 $post_id_to_remove = null;
1181
1182 // Search through caches to find matching post
1183 foreach ($pinecone_cache as $post_id => $cache_data) {
1184 if (isset($cache_data['db_id']) && $cache_data['db_id'] === $vector_id) {
1185 $post_id_to_remove = $post_id;
1186 break;
1187 }
1188 }
1189
1190 // Also check the processed cache
1191 if (!$post_id_to_remove) {
1192 foreach ($processed_cache as $post_id => $cache_data) {
1193 if (isset($cache_data['db_id']) && $cache_data['db_id'] === $vector_id) {
1194 $post_id_to_remove = $post_id;
1195 break;
1196 }
1197 }
1198 }
1199
1200 // If we found the post ID, remove it from both caches
1201 if ($post_id_to_remove) {
1202 unset($pinecone_cache[$post_id_to_remove]);
1203 unset($processed_cache[$post_id_to_remove]);
1204
1205 update_option('mxchat_pinecone_processed_cache', $pinecone_cache);
1206 update_option('mxchat_processed_content_cache', $processed_cache);
1207
1208 //error_log('Removed post ID ' . $post_id_to_remove . ' from processed content caches');
1209 } else {
1210 // If we can't find by vector ID, we might need to reconstruct the URL
1211 // and find the post ID that way
1212 //error_log('Could not find post ID for vector ID: ' . $vector_id);
1213 }
1214 }
1215
1216
1217 /**
1218 * Retrieves and caches Pinecone API processed content
1219 */
1220 public function mxchat_get_pinecone_processed_content($pinecone_options) {
1221 // First check local cache for immediate updates
1222 $cached_data = get_option('mxchat_pinecone_processed_cache', array());
1223
1224 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1225 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1226
1227 if (empty($api_key) || empty($host)) {
1228 // Return only cached data if API credentials are missing
1229 return $cached_data;
1230 }
1231
1232 $pinecone_data = array();
1233
1234 try {
1235 // Method 1: Try to get vectors using cached vector IDs first
1236 $cached_vector_ids = get_option('mxchat_pinecone_vector_ids_cache', array());
1237
1238 if (!empty($cached_vector_ids)) {
1239 $pinecone_data = $this->fetch_pinecone_vectors_by_ids($pinecone_options, $cached_vector_ids);
1240 }
1241
1242 // Method 2: If no cached IDs or fetch failed, use scanning approach
1243 if (empty($pinecone_data)) {
1244 $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options);
1245 }
1246
1247 // Method 3: Final fallback - try stats endpoint (if available)
1248 if (empty($pinecone_data)) {
1249 $stats_url = "https://{$host}/describe_index_stats";
1250
1251 $response = wp_remote_post($stats_url, array(
1252 'headers' => array(
1253 'Api-Key' => $api_key,
1254 'Content-Type' => 'application/json'
1255 ),
1256 'body' => json_encode(array()),
1257 'timeout' => 30
1258 ));
1259
1260 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1261 $body = wp_remote_retrieve_body($response);
1262 $stats_data = json_decode($body, true);
1263
1264 // Log stats for debugging but don't rely on them for vector listing
1265 //error_log('Pinecone index stats: ' . print_r($stats_data, true));
1266 }
1267 }
1268
1269 } catch (Exception $e) {
1270 //error_log('Pinecone processed content exception: ' . $e->getMessage());
1271 }
1272
1273 // Merge cached data with Pinecone data
1274 // Cache takes priority for recent updates (within last 5 minutes)
1275 $merged_data = $pinecone_data;
1276
1277 foreach ($cached_data as $post_id => $cache_item) {
1278 $cache_timestamp = $cache_item['timestamp'] ?? 0;
1279 $time_diff = current_time('timestamp') - $cache_timestamp;
1280
1281 // If cache item is recent (less than 5 minutes), prioritize it
1282 if ($time_diff < 300) { // 5 minutes = 300 seconds
1283 $merged_data[$post_id] = $cache_item;
1284 } else {
1285 // If not in Pinecone data and cache is old, keep cache but mark as potentially stale
1286 if (!isset($merged_data[$post_id])) {
1287 $merged_data[$post_id] = $cache_item;
1288 }
1289 }
1290 }
1291
1292 return $merged_data;
1293 }
1294
1295
1296 // ========================================
1297 // HELPER METHODS
1298 // ========================================
1299
1300 /**
1301 * Validates Pinecone API credentials
1302 */
1303 private function mxchat_validate_pinecone_credentials($api_key, $host) {
1304 if (empty($api_key) || empty($host)) {
1305 return false;
1306 }
1307 return true;
1308 }
1309
1310 /**
1311 * Get Pinecone API credentials from options
1312 */
1313 private function mxchat_get_pinecone_credentials() {
1314 $options = get_option('mxchat_options', array());
1315 return array(
1316 'api_key' => isset($options['pinecone_api_key']) ? $options['pinecone_api_key'] : '',
1317 'host' => isset($options['pinecone_host']) ? $options['pinecone_host'] : ''
1318 );
1319 }
1320
1321 /**
1322 * Log Pinecone operation errors
1323 */
1324 private function log_pinecone_error($operation, $error_message) {
1325 //error_log("MxChat Pinecone {$operation} Error: " . $error_message);
1326 }
1327
1328 // ========================================
1329 // STATIC ACCESS METHODS (for backward compatibility)
1330 // ========================================
1331
1332 /**
1333 * Get singleton instance
1334 */
1335 public static function get_instance() {
1336 static $instance = null;
1337 if ($instance === null) {
1338 $instance = new self();
1339 }
1340 return $instance;
1341 }
1342 }
1343
1344 // Initialize the Pinecone manager
1345 $mxchat_pinecone_manager = MxChat_Pinecone_Manager::get_instance();
1346