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

1,825 lines 65.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 records from Pinecone with bot-specific filtering
29 * UPDATED 2.6.1: Optimized for large datasets - uses server-side pagination
30 */
31 public function mxchat_fetch_pinecone_records($pinecone_options, $search_query = '', $page = 1, $per_page = 20, $bot_id = 'default', $content_type = '') {
32 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
33 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
34 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
35
36 if (empty($api_key) || empty($host)) {
37 return array('data' => array(), 'total' => 0, 'total_in_database' => 0, 'showing_recent_only' => false);
38 }
39
40 try {
41 // Get total count for the banner message (bot-specific) - lightweight call
42 $total_in_database = $this->mxchat_get_pinecone_total_count($pinecone_options, $bot_id);
43
44 // For large databases, use optimized paginated fetching
45 // Only fetch what we need for the current page, not all 1K records
46 if ($total_in_database > 500) {
47 $result = $this->mxchat_fetch_pinecone_page_optimized($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type);
48 $result['total_in_database'] = $total_in_database;
49 $result['showing_recent_only'] = true; // Always show banner when we're limiting results
50 return $result;
51 }
52
53 // For smaller databases, use the existing approach but with safety limits
54 $all_records = $this->mxchat_get_recent_entries_safe($pinecone_options, $bot_id, 500);
55
56 // Filter by content type if provided
57 if (!empty($content_type)) {
58 $all_records = array_filter($all_records, function($record) use ($content_type) {
59 $record_type = $record->type ?? 'content';
60 return $record_type === $content_type;
61 });
62 $all_records = array_values($all_records); // Re-index array
63 }
64
65 // Filter by search query if provided
66 if (!empty($search_query)) {
67 $all_records = array_filter($all_records, function($record) use ($search_query) {
68 $content = $record->article_content ?? '';
69 $source_url = $record->source_url ?? '';
70 return stripos($content, $search_query) !== false || stripos($source_url, $search_query) !== false;
71 });
72 $all_records = array_values($all_records); // Re-index array
73 }
74
75 // Handle pagination
76 $total = count($all_records);
77 $offset = ($page - 1) * $per_page;
78 $paged_records = array_slice($all_records, $offset, $per_page);
79
80 return array(
81 'data' => $paged_records,
82 'total' => $total,
83 'total_in_database' => $total_in_database,
84 'showing_recent_only' => ($total_in_database > 500)
85 );
86
87 } catch (Exception $e) {
88 error_log('MxChat Pinecone fetch error: ' . $e->getMessage());
89 return array('data' => array(), 'total' => 0, 'total_in_database' => 0, 'showing_recent_only' => false);
90 }
91 }
92
93 /**
94 * Optimized fetch for large Pinecone databases
95 * ADDED 2.6.1: Prevents crashes with large datasets
96 * UPDATED 2.6.1: When searching, uses semantic search with embedded query for accurate results across all 13K+ records
97 */
98 private function mxchat_fetch_pinecone_page_optimized($pinecone_options, $search_query = '', $page = 1, $per_page = 20, $bot_id = 'default', $content_type = '') {
99 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
100 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
101 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
102
103 try {
104 // If user is searching, use semantic search with embedded query
105 // This searches ALL records in Pinecone, not just fetched ones
106 if (!empty($search_query)) {
107 return $this->mxchat_semantic_search_pinecone($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type);
108 }
109
110 // For browsing (no search), use Pinecone's list endpoint for true pagination
111 return $this->mxchat_list_pinecone_records($pinecone_options, $page, $per_page, $bot_id, $content_type);
112
113 } catch (Exception $e) {
114 error_log('MxChat optimized fetch exception: ' . $e->getMessage());
115 return array('data' => array(), 'total' => 0);
116 }
117 }
118
119 /**
120 * Semantic search across ALL Pinecone records using embedded search query
121 * This allows users to find any of their 13K+ products by searching
122 * ADDED 2.6.1
123 */
124 private function mxchat_semantic_search_pinecone($pinecone_options, $search_query, $page = 1, $per_page = 20, $bot_id = 'default', $content_type = '') {
125 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
126 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
127 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
128
129 // Get embedding for the search query
130 $query_embedding = $this->mxchat_get_search_embedding($search_query);
131
132 if (empty($query_embedding)) {
133 // Fallback to text-based search if embedding fails
134 return $this->mxchat_text_search_fallback($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type);
135 }
136
137 $query_url = "https://{$host}/query";
138
139 // Fetch more results to allow for filtering and pagination
140 $fetch_limit = min(($page * $per_page) + 100, 500);
141
142 $query_data = array(
143 'includeMetadata' => true,
144 'includeValues' => false,
145 'topK' => $fetch_limit,
146 'vector' => $query_embedding
147 );
148
149 if (!empty($namespace)) {
150 $query_data['namespace'] = $namespace;
151 }
152
153 // Add metadata filter for content type if specified
154 if (!empty($content_type)) {
155 $query_data['filter'] = array(
156 'type' => array('$eq' => $content_type)
157 );
158 }
159
160 $response = wp_remote_post($query_url, array(
161 'headers' => array(
162 'Api-Key' => $api_key,
163 'Content-Type' => 'application/json'
164 ),
165 'body' => json_encode($query_data),
166 'timeout' => 20
167 ));
168
169 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
170 return $this->mxchat_text_search_fallback($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type);
171 }
172
173 $body = wp_remote_retrieve_body($response);
174 $data = json_decode($body, true);
175
176 $records = array();
177 if (isset($data['matches'])) {
178 foreach ($data['matches'] as $match) {
179 $metadata = $match['metadata'] ?? array();
180 $created_at = $metadata['created_at'] ?? $metadata['last_updated'] ?? $metadata['timestamp'] ?? time();
181 if (!is_numeric($created_at)) {
182 $created_at = strtotime($created_at) ?: time();
183 }
184
185 $records[] = (object) array(
186 'id' => $match['id'] ?? '',
187 'article_content' => $metadata['text'] ?? '',
188 'source_url' => $metadata['source_url'] ?? '',
189 'role_restriction' => $metadata['role_restriction'] ?? 'public',
190 'type' => $metadata['type'] ?? 'content',
191 'bot_id' => $bot_id,
192 'created_at' => $created_at,
193 'data_source' => 'pinecone',
194 'relevance_score' => $match['score'] ?? 0
195 );
196 }
197 }
198
199 // For semantic search, results are already sorted by relevance (score)
200 // No need to re-sort by date
201
202 $total = count($records);
203 $offset = ($page - 1) * $per_page;
204 $paged_records = array_slice($records, $offset, $per_page);
205
206 $this->mxchat_batch_fetch_role_restrictions($paged_records, $bot_id);
207
208 return array(
209 'data' => $paged_records,
210 'total' => $total
211 );
212 }
213
214 /**
215 * Get embedding vector for a search query
216 * Uses the same embedding model configured for the knowledge base
217 * ADDED 2.6.1
218 */
219 private function mxchat_get_search_embedding($search_query) {
220 $options = get_option('mxchat_options', array());
221 $embedding_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
222
223 // Determine which API to use based on model
224 if (strpos($embedding_model, 'voyage-') === 0) {
225 return $this->mxchat_get_voyage_embedding($search_query, $options, $embedding_model);
226 } elseif (strpos($embedding_model, 'gemini-') === 0) {
227 return $this->mxchat_get_gemini_embedding($search_query, $options, $embedding_model);
228 } else {
229 return $this->mxchat_get_openai_embedding($search_query, $options, $embedding_model);
230 }
231 }
232
233 /**
234 * Get OpenAI embedding for search query
235 */
236 private function mxchat_get_openai_embedding($text, $options, $model) {
237 $api_key = $options['api_key'] ?? '';
238 if (empty($api_key)) {
239 return null;
240 }
241
242 $response = wp_remote_post('https://api.openai.com/v1/embeddings', array(
243 'headers' => array(
244 'Authorization' => 'Bearer ' . $api_key,
245 'Content-Type' => 'application/json'
246 ),
247 'body' => json_encode(array(
248 'model' => $model,
249 'input' => $text
250 )),
251 'timeout' => 15
252 ));
253
254 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
255 return null;
256 }
257
258 $body = json_decode(wp_remote_retrieve_body($response), true);
259 return $body['data'][0]['embedding'] ?? null;
260 }
261
262 /**
263 * Get Voyage AI embedding for search query
264 */
265 private function mxchat_get_voyage_embedding($text, $options, $model) {
266 $api_key = $options['voyage_api_key'] ?? '';
267 if (empty($api_key)) {
268 return null;
269 }
270
271 $request_body = array(
272 'model' => $model,
273 'input' => $text,
274 'input_type' => 'query'
275 );
276
277 // Add output dimensions for voyage-3-large if configured
278 if (strpos($model, 'voyage-3-large') === 0 && !empty($options['voyage_output_dimension'])) {
279 $request_body['output_dimension'] = intval($options['voyage_output_dimension']);
280 }
281
282 $response = wp_remote_post('https://api.voyageai.com/v1/embeddings', array(
283 'headers' => array(
284 'Authorization' => 'Bearer ' . $api_key,
285 'Content-Type' => 'application/json'
286 ),
287 'body' => json_encode($request_body),
288 'timeout' => 15
289 ));
290
291 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
292 return null;
293 }
294
295 $body = json_decode(wp_remote_retrieve_body($response), true);
296 return $body['data'][0]['embedding'] ?? null;
297 }
298
299 /**
300 * Get Google Gemini embedding for search query
301 */
302 private function mxchat_get_gemini_embedding($text, $options, $model) {
303 $api_key = $options['gemini_api_key'] ?? '';
304 if (empty($api_key)) {
305 return null;
306 }
307
308 $url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:embedContent?key={$api_key}";
309
310 $request_body = array(
311 'model' => "models/{$model}",
312 'content' => array(
313 'parts' => array(
314 array('text' => $text)
315 )
316 ),
317 'taskType' => 'RETRIEVAL_QUERY'
318 );
319
320 // Add output dimensions if configured
321 if (!empty($options['gemini_output_dimension'])) {
322 $request_body['outputDimensionality'] = intval($options['gemini_output_dimension']);
323 }
324
325 $response = wp_remote_post($url, array(
326 'headers' => array(
327 'Content-Type' => 'application/json'
328 ),
329 'body' => json_encode($request_body),
330 'timeout' => 15
331 ));
332
333 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
334 return null;
335 }
336
337 $body = json_decode(wp_remote_retrieve_body($response), true);
338 return $body['embedding']['values'] ?? null;
339 }
340
341 /**
342 * Fallback text search when embedding fails
343 * Fetches more records and filters by text match
344 * ADDED 2.6.1
345 */
346 private function mxchat_text_search_fallback($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type) {
347 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
348 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
349 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
350
351 $query_url = "https://{$host}/query";
352 $query_vector = $this->mxchat_generate_optimized_query_vector();
353
354 // Fetch more records to search through
355 $query_data = array(
356 'includeMetadata' => true,
357 'includeValues' => false,
358 'topK' => 1000, // Fetch more for text search
359 'vector' => $query_vector
360 );
361
362 if (!empty($namespace)) {
363 $query_data['namespace'] = $namespace;
364 }
365
366 $response = wp_remote_post($query_url, array(
367 'headers' => array(
368 'Api-Key' => $api_key,
369 'Content-Type' => 'application/json'
370 ),
371 'body' => json_encode($query_data),
372 'timeout' => 20
373 ));
374
375 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
376 return array('data' => array(), 'total' => 0);
377 }
378
379 $body = wp_remote_retrieve_body($response);
380 $data = json_decode($body, true);
381
382 $records = array();
383 $search_lower = strtolower($search_query);
384
385 if (isset($data['matches'])) {
386 foreach ($data['matches'] as $match) {
387 $metadata = $match['metadata'] ?? array();
388
389 // Filter by content type
390 if (!empty($content_type)) {
391 $record_type = $metadata['type'] ?? 'content';
392 if ($record_type !== $content_type) {
393 continue;
394 }
395 }
396
397 // Text search filter
398 $content = strtolower($metadata['text'] ?? '');
399 $source_url = strtolower($metadata['source_url'] ?? '');
400 if (strpos($content, $search_lower) === false && strpos($source_url, $search_lower) === false) {
401 continue;
402 }
403
404 $created_at = $metadata['created_at'] ?? $metadata['last_updated'] ?? $metadata['timestamp'] ?? time();
405 if (!is_numeric($created_at)) {
406 $created_at = strtotime($created_at) ?: time();
407 }
408
409 $records[] = (object) array(
410 'id' => $match['id'] ?? '',
411 'article_content' => $metadata['text'] ?? '',
412 'source_url' => $metadata['source_url'] ?? '',
413 'role_restriction' => $metadata['role_restriction'] ?? 'public',
414 'type' => $metadata['type'] ?? 'content',
415 'bot_id' => $bot_id,
416 'created_at' => $created_at,
417 'data_source' => 'pinecone'
418 );
419 }
420 }
421
422 // Sort by date for text search results
423 usort($records, function($a, $b) {
424 return $b->created_at - $a->created_at;
425 });
426
427 $total = count($records);
428 $offset = ($page - 1) * $per_page;
429 $paged_records = array_slice($records, $offset, $per_page);
430
431 $this->mxchat_batch_fetch_role_restrictions($paged_records, $bot_id);
432
433 return array(
434 'data' => $paged_records,
435 'total' => $total
436 );
437 }
438
439 /**
440 * List Pinecone records using the list endpoint for true pagination (no search)
441 * This allows browsing through all 13K+ records page by page
442 * ADDED 2.6.1
443 */
444 private function mxchat_list_pinecone_records($pinecone_options, $page = 1, $per_page = 20, $bot_id = 'default', $content_type = '') {
445 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
446 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
447 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
448
449 // Pinecone's list endpoint returns vector IDs with pagination
450 // We then fetch the metadata for those specific IDs
451 $list_url = "https://{$host}/vectors/list";
452
453 // Calculate pagination token from page number
454 // Pinecone uses cursor-based pagination, so we need to handle this differently
455 $limit = $per_page * 2; // Fetch extra to account for filtering
456
457 $list_params = array(
458 'limit' => $limit
459 );
460
461 if (!empty($namespace)) {
462 $list_params['namespace'] = $namespace;
463 }
464
465 // For pages beyond first, we need to use pagination_token
466 // Store/retrieve pagination tokens in transients
467 $pagination_key = 'mxchat_pinecone_page_' . md5($host . $namespace . $content_type);
468
469 if ($page > 1) {
470 $stored_tokens = get_transient($pagination_key);
471 if ($stored_tokens && isset($stored_tokens[$page])) {
472 $list_params['paginationToken'] = $stored_tokens[$page];
473 }
474 }
475
476 $response = wp_remote_post($list_url, array(
477 'headers' => array(
478 'Api-Key' => $api_key,
479 'Content-Type' => 'application/json'
480 ),
481 'body' => json_encode($list_params),
482 'timeout' => 15
483 ));
484
485 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
486 // Fallback to query-based approach
487 return $this->mxchat_query_based_list($pinecone_options, $page, $per_page, $bot_id, $content_type);
488 }
489
490 $body = wp_remote_retrieve_body($response);
491 $data = json_decode($body, true);
492
493 // Store pagination token for next page
494 if (!empty($data['pagination']['next'])) {
495 $stored_tokens = get_transient($pagination_key) ?: array();
496 $stored_tokens[$page + 1] = $data['pagination']['next'];
497 set_transient($pagination_key, $stored_tokens, 300); // 5 minute cache
498 }
499
500 $vector_ids = array();
501 if (isset($data['vectors'])) {
502 foreach ($data['vectors'] as $vector) {
503 $vector_ids[] = $vector['id'];
504 }
505 }
506
507 // If list endpoint returned empty, fall back to query-based approach
508 if (empty($vector_ids)) {
509 return $this->mxchat_query_based_list($pinecone_options, $page, $per_page, $bot_id, $content_type);
510 }
511
512 // Fetch metadata for these vector IDs
513 return $this->mxchat_fetch_vectors_by_ids_for_list($pinecone_options, $vector_ids, $page, $per_page, $bot_id, $content_type);
514 }
515
516 /**
517 * Query-based listing fallback when list endpoint fails
518 * ADDED 2.6.1
519 */
520 private function mxchat_query_based_list($pinecone_options, $page, $per_page, $bot_id, $content_type) {
521 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
522 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
523 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
524
525 $query_url = "https://{$host}/query";
526 $query_vector = $this->mxchat_generate_optimized_query_vector();
527
528 // Fetch records - use higher limit to cover large databases
529 // Pinecone query API supports up to 10,000 topK
530 // We fetch more than needed to get accurate total count and enable pagination
531 $fetch_limit = 5000;
532
533 $query_data = array(
534 'includeMetadata' => true,
535 'includeValues' => false,
536 'topK' => $fetch_limit,
537 'vector' => $query_vector
538 );
539
540 if (!empty($namespace)) {
541 $query_data['namespace'] = $namespace;
542 }
543
544 // Add content type filter if specified
545 if (!empty($content_type)) {
546 $query_data['filter'] = array(
547 'type' => array('$eq' => $content_type)
548 );
549 }
550
551 $response = wp_remote_post($query_url, array(
552 'headers' => array(
553 'Api-Key' => $api_key,
554 'Content-Type' => 'application/json'
555 ),
556 'body' => json_encode($query_data),
557 'timeout' => 30
558 ));
559
560 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
561 return array('data' => array(), 'total' => 0);
562 }
563
564 $body = wp_remote_retrieve_body($response);
565 $data = json_decode($body, true);
566
567 $records = array();
568 if (isset($data['matches'])) {
569 foreach ($data['matches'] as $match) {
570 $metadata = $match['metadata'] ?? array();
571
572 $created_at = $metadata['created_at'] ?? $metadata['last_updated'] ?? $metadata['timestamp'] ?? time();
573 if (!is_numeric($created_at)) {
574 $created_at = strtotime($created_at) ?: time();
575 }
576
577 $records[] = (object) array(
578 'id' => $match['id'] ?? '',
579 'article_content' => $metadata['text'] ?? '',
580 'source_url' => $metadata['source_url'] ?? '',
581 'role_restriction' => $metadata['role_restriction'] ?? 'public',
582 'type' => $metadata['type'] ?? 'content',
583 'bot_id' => $bot_id,
584 'created_at' => $created_at,
585 'data_source' => 'pinecone'
586 );
587 }
588 }
589
590 // Sort by created_at (newest first)
591 usort($records, function($a, $b) {
592 return $b->created_at - $a->created_at;
593 });
594
595 $total = count($records);
596 $offset = ($page - 1) * $per_page;
597 $paged_records = array_slice($records, $offset, $per_page);
598
599 $this->mxchat_batch_fetch_role_restrictions($paged_records, $bot_id);
600
601 return array(
602 'data' => $paged_records,
603 'total' => $total
604 );
605 }
606
607 /**
608 * Fetch specific vectors by their IDs and format for display
609 * ADDED 2.6.1
610 */
611 private function mxchat_fetch_vectors_by_ids_for_list($pinecone_options, $vector_ids, $page, $per_page, $bot_id, $content_type) {
612 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
613 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
614 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
615
616 $fetch_url = "https://{$host}/vectors/fetch";
617
618 $fetch_data = array(
619 'ids' => $vector_ids
620 );
621
622 if (!empty($namespace)) {
623 $fetch_data['namespace'] = $namespace;
624 }
625
626 $response = wp_remote_post($fetch_url, array(
627 'headers' => array(
628 'Api-Key' => $api_key,
629 'Content-Type' => 'application/json'
630 ),
631 'body' => json_encode($fetch_data),
632 'timeout' => 15
633 ));
634
635 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
636 return array('data' => array(), 'total' => 0);
637 }
638
639 $body = wp_remote_retrieve_body($response);
640 $data = json_decode($body, true);
641
642 $records = array();
643 if (isset($data['vectors'])) {
644 foreach ($data['vectors'] as $vector_id => $vector_data) {
645 $metadata = $vector_data['metadata'] ?? array();
646
647 // Filter by content type if specified
648 if (!empty($content_type)) {
649 $record_type = $metadata['type'] ?? 'content';
650 if ($record_type !== $content_type) {
651 continue;
652 }
653 }
654
655 $created_at = $metadata['created_at'] ?? $metadata['last_updated'] ?? $metadata['timestamp'] ?? time();
656 if (!is_numeric($created_at)) {
657 $created_at = strtotime($created_at) ?: time();
658 }
659
660 $records[] = (object) array(
661 'id' => $vector_id,
662 'article_content' => $metadata['text'] ?? '',
663 'source_url' => $metadata['source_url'] ?? '',
664 'role_restriction' => $metadata['role_restriction'] ?? 'public',
665 'type' => $metadata['type'] ?? 'content',
666 'bot_id' => $bot_id,
667 'created_at' => $created_at,
668 'data_source' => 'pinecone'
669 );
670 }
671 }
672
673 // Sort by date
674 usort($records, function($a, $b) {
675 return $b->created_at - $a->created_at;
676 });
677
678 $total = count($records);
679 $paged_records = array_slice($records, 0, $per_page);
680
681 $this->mxchat_batch_fetch_role_restrictions($paged_records, $bot_id);
682
683 return array(
684 'data' => $paged_records,
685 'total' => $total
686 );
687 }
688
689 /**
690 * Generate a single optimized query vector for fetching records
691 * Uses a center-weighted approach for best coverage
692 */
693 private function mxchat_generate_optimized_query_vector() {
694 $dimensions = $this->mxchat_get_embedding_dimensions();
695 $vector = array();
696
697 // Create a normalized center-weighted vector
698 $center = $dimensions / 2;
699 for ($i = 0; $i < $dimensions; $i++) {
700 $distance = abs($i - $center) / $center;
701 $vector[] = (1 - $distance) * 0.5;
702 }
703
704 // Normalize to unit length
705 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $vector)));
706 if ($magnitude > 0) {
707 $vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $vector);
708 }
709
710 return $vector;
711 }
712
713 /**
714 * Batch fetch role restrictions for a set of records
715 * Uses a single query instead of N queries
716 * ADDED 2.6.1: Prevents N+1 query problem
717 */
718 private function mxchat_batch_fetch_role_restrictions(&$records, $bot_id = 'default') {
719 if (empty($records)) {
720 return;
721 }
722
723 global $wpdb;
724 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
725
726 // Check if table exists
727 $table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$roles_table}'");
728 if (!$table_exists) {
729 return;
730 }
731
732 // Get all vector IDs that need role lookup
733 $vector_ids = array();
734 foreach ($records as $record) {
735 if (empty($record->role_restriction) || $record->role_restriction === 'public') {
736 $vector_ids[] = $record->id;
737 }
738 }
739
740 if (empty($vector_ids)) {
741 return;
742 }
743
744 // Check if bot_id column exists
745 $columns = $wpdb->get_col("SHOW COLUMNS FROM {$roles_table}");
746 $has_bot_id = in_array('bot_id', $columns);
747
748 // Build single query with IN clause
749 $placeholders = implode(',', array_fill(0, count($vector_ids), '%s'));
750
751 if ($has_bot_id) {
752 $query = $wpdb->prepare(
753 "SELECT vector_id, role_restriction FROM {$roles_table} WHERE vector_id IN ({$placeholders}) AND bot_id = %s",
754 array_merge($vector_ids, array($bot_id))
755 );
756 } else {
757 $query = $wpdb->prepare(
758 "SELECT vector_id, role_restriction FROM {$roles_table} WHERE vector_id IN ({$placeholders})",
759 $vector_ids
760 );
761 }
762
763 $results = $wpdb->get_results($query, OBJECT_K);
764
765 // Apply role restrictions to records
766 foreach ($records as &$record) {
767 if (isset($results[$record->id])) {
768 $record->role_restriction = $results[$record->id]->role_restriction;
769 }
770 }
771 }
772
773 /**
774 * Safe version of get_recent_entries with memory limits
775 * ADDED 2.6.1: Prevents memory exhaustion
776 */
777 private function mxchat_get_recent_entries_safe($pinecone_options, $bot_id = 'default', $limit = 500) {
778 global $wpdb;
779
780 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
781 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
782 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
783
784 if (empty($api_key) || empty($host)) {
785 return array();
786 }
787
788 try {
789 $all_records = array();
790 $seen_ids = array();
791 $query_url = "https://{$host}/query";
792
793 // Use only 2 query vectors instead of 5 for better performance
794 $fixed_vectors = array_slice($this->mxchat_generate_fixed_query_vectors(), 0, 2);
795
796 foreach ($fixed_vectors as $query_vector) {
797 // Limit topK to prevent memory issues
798 $topK = min(500, $limit);
799
800 $query_data = array(
801 'includeMetadata' => true,
802 'includeValues' => false,
803 'topK' => $topK,
804 'vector' => $query_vector
805 );
806
807 if (!empty($namespace)) {
808 $query_data['namespace'] = $namespace;
809 }
810
811 $response = wp_remote_post($query_url, array(
812 'headers' => array(
813 'Api-Key' => $api_key,
814 'Content-Type' => 'application/json'
815 ),
816 'body' => json_encode($query_data),
817 'timeout' => 15
818 ));
819
820 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
821 $body = wp_remote_retrieve_body($response);
822 $data = json_decode($body, true);
823
824 if (isset($data['matches'])) {
825 foreach ($data['matches'] as $match) {
826 $match_id = $match['id'] ?? '';
827 if (!empty($match_id) && !isset($seen_ids[$match_id])) {
828 $metadata = $match['metadata'] ?? array();
829
830 $created_at = $metadata['created_at'] ?? $metadata['last_updated'] ?? $metadata['timestamp'] ?? time();
831 if (!is_numeric($created_at)) {
832 $created_at = strtotime($created_at) ?: time();
833 }
834
835 $all_records[] = (object) array(
836 'id' => $match_id,
837 'article_content' => $metadata['text'] ?? '',
838 'source_url' => $metadata['source_url'] ?? '',
839 'role_restriction' => $metadata['role_restriction'] ?? 'public',
840 'type' => $metadata['type'] ?? 'content',
841 'bot_id' => $bot_id,
842 'created_at' => $created_at,
843 'data_source' => 'pinecone'
844 );
845
846 $seen_ids[$match_id] = true;
847
848 // Stop if we've reached our limit
849 if (count($all_records) >= $limit) {
850 break 2;
851 }
852 }
853 }
854 }
855 }
856
857 // Minimal delay between requests
858 usleep(50000); // 0.05 second delay
859 }
860
861 // Sort by created_at (newest first) and apply limit
862 usort($all_records, function($a, $b) {
863 return $b->created_at - $a->created_at;
864 });
865
866 $limited_records = array_slice($all_records, 0, $limit);
867
868 // Batch fetch role restrictions
869 $this->mxchat_batch_fetch_role_restrictions($limited_records, $bot_id);
870
871 return $limited_records;
872
873 } catch (Exception $e) {
874 error_log('MxChat safe fetch exception: ' . $e->getMessage());
875 return array();
876 }
877 }
878 /**
879 * Get embedding dimensions based on the selected model
880 * ADD THIS NEW FUNCTION
881 */
882 private function mxchat_get_embedding_dimensions() {
883 $options = get_option('mxchat_options', array());
884 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
885
886 // Define dimensions for different models
887 $model_dimensions = array(
888 'text-embedding-ada-002' => 1536,
889 'text-embedding-3-small' => 1536,
890 'text-embedding-3-large' => 3072,
891 'voyage-2' => 1024,
892 'voyage-large-2' => 1536,
893 'voyage-3-large' => 2048,
894 'gemini-embedding-001' => 1536,
895 );
896
897 // Check if it's a voyage model with custom dimensions
898 if (strpos($selected_model, 'voyage-3-large') === 0) {
899 $custom_dimensions = $options['voyage_output_dimension'] ?? 2048;
900 return intval($custom_dimensions);
901 }
902
903 // Check if it's a gemini model with custom dimensions
904 if (strpos($selected_model, 'gemini-embedding') === 0) {
905 $custom_dimensions = $options['gemini_output_dimension'] ?? 1536;
906 return intval($custom_dimensions);
907 }
908
909 // Return known dimensions or default to 1536
910 return $model_dimensions[$selected_model] ?? 1536;
911 }
912
913 /**
914 * Generate random unit vector with correct dimensions
915 * ADD THIS NEW FUNCTION
916 */
917 private function mxchat_generate_random_vector() {
918 $dimensions = $this->mxchat_get_embedding_dimensions();
919
920 $random_vector = array();
921 for ($i = 0; $i < $dimensions; $i++) {
922 $random_vector[] = (rand(-1000, 1000) / 1000.0);
923 }
924
925 // Normalize the vector to unit length
926 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
927 if ($magnitude > 0) {
928 $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
929 }
930
931 return $random_vector;
932 }
933
934
935 /**
936 * Get recent entries from Pinecone
937 * UPDATED 2.6.1: Now uses safe version with memory limits to prevent crashes
938 * @deprecated Use mxchat_get_recent_entries_safe() instead for new code
939 */
940 private function mxchat_get_recent_1k_entries($pinecone_options, $bot_id = 'default') {
941 // Delegate to the safe version with a reasonable limit
942 // This prevents crashes with large datasets (13K+ products)
943 return $this->mxchat_get_recent_entries_safe($pinecone_options, $bot_id, 500);
944 }
945 /**
946 * Generate fixed query vectors for consistent results
947 */
948 private function mxchat_generate_fixed_query_vectors() {
949 $dimensions = $this->mxchat_get_embedding_dimensions();
950 $vectors = array();
951
952 // Create 5 fixed vectors with different patterns for better coverage
953 $patterns = array(
954 'zeros_with_ones' => 0.1, // Mostly zeros with some 1s
955 'ascending' => 0.2, // Ascending pattern
956 'descending' => 0.3, // Descending pattern
957 'alternating' => 0.4, // Alternating positive/negative
958 'center_weighted' => 0.5 // Higher values in center
959 );
960
961 foreach ($patterns as $pattern_name => $seed) {
962 $vector = array();
963
964 for ($i = 0; $i < $dimensions; $i++) {
965 switch ($pattern_name) {
966 case 'zeros_with_ones':
967 $vector[] = ($i % 10 === 0) ? 1.0 : 0.0;
968 break;
969 case 'ascending':
970 $vector[] = ($i / $dimensions) * 2 - 1; // Range -1 to 1
971 break;
972 case 'descending':
973 $vector[] = (($dimensions - $i) / $dimensions) * 2 - 1;
974 break;
975 case 'alternating':
976 $vector[] = ($i % 2 === 0) ? $seed : -$seed;
977 break;
978 case 'center_weighted':
979 $center = $dimensions / 2;
980 $distance = abs($i - $center) / $center;
981 $vector[] = (1 - $distance) * $seed;
982 break;
983 }
984 }
985
986 // Normalize the vector to unit length
987 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $vector)));
988 if ($magnitude > 0) {
989 $vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $vector);
990 }
991
992 $vectors[] = $vector;
993 }
994
995 return $vectors;
996 }
997
998
999 /**
1000 * Scan Pinecone for processed content
1001 * UPDATED 2.6.2: Uses direct ID lookup via fetch API instead of random vector scanning
1002 * This removes the 10K record limit and scales to any database size
1003 *
1004 * @param array $pinecone_options Pinecone configuration options
1005 * @param array $post_ids Optional array of specific post IDs to check (if empty, checks all published posts)
1006 * @return array Processed data keyed by post ID
1007 */
1008 public function mxchat_scan_pinecone_for_processed_content($pinecone_options, $post_ids = array()) {
1009 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1010 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1011
1012 if (empty($api_key) || empty($host)) {
1013 return array();
1014 }
1015
1016 try {
1017 // If no specific post IDs provided, get all published posts
1018 if (empty($post_ids)) {
1019 $posts = get_posts(array(
1020 'post_type' => 'any',
1021 'post_status' => 'publish',
1022 'posts_per_page' => -1,
1023 'fields' => 'ids',
1024 'no_found_rows' => true,
1025 'update_post_meta_cache' => false,
1026 'update_post_term_cache' => false,
1027 ));
1028 $post_ids = $posts;
1029 }
1030
1031 if (empty($post_ids)) {
1032 return array();
1033 }
1034
1035 // Build a map of vector_id => post data for lookup
1036 $vector_id_map = array();
1037 foreach ($post_ids as $post_id) {
1038 $permalink = get_permalink($post_id);
1039 if ($permalink) {
1040 $vector_id = md5($permalink);
1041 $vector_id_map[$vector_id] = array(
1042 'post_id' => $post_id,
1043 'url' => $permalink
1044 );
1045 }
1046 }
1047
1048 if (empty($vector_id_map)) {
1049 return array();
1050 }
1051
1052 // Batch check Pinecone using fetch API (max 1000 IDs per request)
1053 $all_vector_ids = array_keys($vector_id_map);
1054 $chunks = array_chunk($all_vector_ids, 1000);
1055 $processed_data = array();
1056
1057 foreach ($chunks as $chunk) {
1058 $fetch_url = "https://{$host}/vectors/fetch";
1059
1060 $response = wp_remote_post($fetch_url, array(
1061 'headers' => array(
1062 'Api-Key' => $api_key,
1063 'Content-Type' => 'application/json'
1064 ),
1065 'body' => json_encode(array('ids' => $chunk)),
1066 'timeout' => 30
1067 ));
1068
1069 if (is_wp_error($response)) {
1070 continue;
1071 }
1072
1073 $response_code = wp_remote_retrieve_response_code($response);
1074 if ($response_code !== 200) {
1075 continue;
1076 }
1077
1078 $body = wp_remote_retrieve_body($response);
1079 $data = json_decode($body, true);
1080
1081 // Process returned vectors
1082 if (isset($data['vectors']) && is_array($data['vectors'])) {
1083 foreach ($data['vectors'] as $vector_id => $vector_data) {
1084 if (isset($vector_id_map[$vector_id])) {
1085 $post_info = $vector_id_map[$vector_id];
1086 $post_id = $post_info['post_id'];
1087 $metadata = $vector_data['metadata'] ?? array();
1088
1089 $created_at = $metadata['created_at'] ?? '';
1090 $processed_date = 'Recently';
1091 $timestamp = current_time('timestamp');
1092
1093 if (!empty($created_at)) {
1094 $ts = is_numeric($created_at) ? $created_at : strtotime($created_at);
1095 if ($ts) {
1096 $timestamp = $ts;
1097 $processed_date = human_time_diff($ts, current_time('timestamp')) . ' ago';
1098 }
1099 }
1100
1101 $processed_data[$post_id] = array(
1102 'db_id' => $vector_id,
1103 'processed_date' => $processed_date,
1104 'url' => $post_info['url'],
1105 'source' => 'pinecone',
1106 'timestamp' => $timestamp
1107 );
1108 }
1109 }
1110 }
1111 }
1112
1113 return $processed_data;
1114
1115 } catch (Exception $e) {
1116 return array();
1117 }
1118 }
1119
1120 /**
1121 * Get total count from Pinecone stats API
1122 * UPDATED: Removed cache fallback reference
1123 */
1124 private function mxchat_get_pinecone_total_count($pinecone_options, $bot_id = 'default') {
1125 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1126 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1127 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
1128
1129 if (empty($api_key) || empty($host)) {
1130 return 0;
1131 }
1132
1133 try {
1134 $stats_url = "https://{$host}/describe_index_stats";
1135
1136 // describe_index_stats doesn't need a body, just the POST request
1137 $response = wp_remote_post($stats_url, array(
1138 'headers' => array(
1139 'Api-Key' => $api_key,
1140 'Content-Type' => 'application/json'
1141 ),
1142 'body' => '{}',
1143 'timeout' => 15
1144 ));
1145
1146 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1147 $body = wp_remote_retrieve_body($response);
1148 $stats_data = json_decode($body, true);
1149
1150 // If namespace is specified, get count from that specific namespace
1151 // Pinecone stats response format: { namespaces: { "ns": { vectorCount: N } }, totalVectorCount: N }
1152 if (!empty($namespace) && isset($stats_data['namespaces'][$namespace]['vectorCount'])) {
1153 $namespace_count = intval($stats_data['namespaces'][$namespace]['vectorCount']);
1154 //error_log('DEBUG: Got namespace-specific count: ' . $namespace_count . ' for namespace: ' . $namespace);
1155 return $namespace_count;
1156 }
1157
1158 // If no namespace specified or namespace not found in response, use total
1159 $total_count = $stats_data['totalVectorCount'] ?? 0;
1160 if ($total_count > 0) {
1161 //error_log('DEBUG: Got total count from stats API: ' . $total_count);
1162 return intval($total_count);
1163 }
1164 }
1165
1166 // If stats API fails, return 0 instead of using cache
1167 return 0;
1168
1169 } catch (Exception $e) {
1170 //error_log('DEBUG: Exception getting total count: ' . $e->getMessage());
1171 return 0;
1172 }
1173 }
1174
1175 /**
1176 * Get bot-specific Pinecone configuration for database operations
1177 */
1178 public function mxchat_get_bot_pinecone_options($bot_id = 'default') {
1179 //error_log('DEBUG: Getting Pinecone options for bot: ' . $bot_id);
1180
1181 // If default bot or multi-bot add-on not active, use default Pinecone config
1182 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1183 $addon_options = get_option('mxchat_pinecone_addon_options', array());
1184 //error_log('DEBUG: Using default Pinecone options');
1185 return $addon_options;
1186 }
1187
1188 // Get bot-specific configuration using the filter
1189 $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1190
1191 //error_log('DEBUG: Bot config from filter: ' . print_r($bot_config, true));
1192
1193 // Check if we got valid bot-specific config
1194 if (!empty($bot_config) && isset($bot_config['use_pinecone']) && $bot_config['use_pinecone']) {
1195 // Convert bot config to the format expected by fetch functions
1196 $pinecone_options = array(
1197 'mxchat_use_pinecone' => '1',
1198 'mxchat_pinecone_api_key' => $bot_config['api_key'] ?? '',
1199 'mxchat_pinecone_host' => $bot_config['host'] ?? '',
1200 'mxchat_pinecone_namespace' => $bot_config['namespace'] ?? '',
1201 'mxchat_pinecone_environment' => '',
1202 'mxchat_pinecone_index' => ''
1203 );
1204
1205 //error_log('DEBUG: Returning bot-specific Pinecone options for bot: ' . $bot_id);
1206 return $pinecone_options;
1207 }
1208
1209 // Fallback to default options if bot-specific config is invalid
1210 //error_log('DEBUG: Bot-specific config invalid, falling back to default');
1211 return get_option('mxchat_pinecone_addon_options', array());
1212 }
1213
1214 /**
1215 * Get bot-specific Pinecone configuration
1216 * Used in the knowledge retrieval functions
1217 */
1218 private function get_bot_pinecone_config($bot_id = 'default') {
1219 //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1220
1221 // If default bot or multi-bot add-on not active, use default Pinecone config
1222 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1223 //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1224 $addon_options = get_option('mxchat_pinecone_addon_options', array());
1225 $config = array(
1226 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
1227 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
1228 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
1229 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
1230 );
1231 //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1232 return $config;
1233 }
1234
1235 //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1236
1237 // Hook for multi-bot add-on to provide bot-specific Pinecone config
1238 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1239
1240 if (!empty($bot_pinecone_config)) {
1241 //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1242 //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1243 //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1244 //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1245 } else {
1246 //error_log("MXCHAT DEBUG: Filter returned empty config!");
1247 }
1248
1249 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1250 }
1251
1252
1253 /**
1254 * Fetches vectors from Pinecone using provided IDs (for content selection feature)
1255 */
1256 public function fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
1257 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1258 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1259
1260 if (empty($api_key) || empty($host) || empty($vector_ids)) {
1261 return array();
1262 }
1263
1264 try {
1265 $fetch_url = "https://{$host}/vectors/fetch";
1266
1267 // Pinecone fetch API allows fetching specific vectors by ID
1268 $fetch_data = array(
1269 'ids' => array_values($vector_ids)
1270 );
1271
1272 $response = wp_remote_post($fetch_url, array(
1273 'headers' => array(
1274 'Api-Key' => $api_key,
1275 'Content-Type' => 'application/json'
1276 ),
1277 'body' => json_encode($fetch_data),
1278 'timeout' => 30
1279 ));
1280
1281 if (is_wp_error($response)) {
1282 return array();
1283 }
1284
1285 $response_code = wp_remote_retrieve_response_code($response);
1286
1287 if ($response_code !== 200) {
1288 return array();
1289 }
1290
1291 $body = wp_remote_retrieve_body($response);
1292 $data = json_decode($body, true);
1293
1294 if (!isset($data['vectors'])) {
1295 return array();
1296 }
1297
1298 $processed_data = array();
1299
1300 foreach ($data['vectors'] as $vector_id => $vector_data) {
1301 $metadata = $vector_data['metadata'] ?? array();
1302 $source_url = $metadata['source_url'] ?? '';
1303
1304 if (!empty($source_url)) {
1305 $post_id = url_to_postid($source_url);
1306 if ($post_id) {
1307 $created_at = $metadata['created_at'] ?? '';
1308 $processed_date = 'Recently'; // Default
1309
1310 if (!empty($created_at)) {
1311 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
1312 if ($timestamp) {
1313 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
1314 }
1315 }
1316
1317 $processed_data[$post_id] = array(
1318 'db_id' => $vector_id,
1319 'processed_date' => $processed_date,
1320 'url' => $source_url,
1321 'source' => 'pinecone',
1322 'timestamp' => $timestamp ?? current_time('timestamp')
1323 );
1324 }
1325 }
1326 }
1327
1328 return $processed_data;
1329
1330 } catch (Exception $e) {
1331 return array();
1332 }
1333 }
1334 // ========================================
1335 // PINECONE DELETE OPERATIONS
1336 // ========================================
1337
1338 /**
1339 * Delete all vectors from Pinecone
1340 */
1341 public function mxchat_delete_all_from_pinecone($pinecone_options) {
1342 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1343 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1344
1345 if (empty($api_key) || empty($host)) {
1346 return array(
1347 'success' => false,
1348 'message' => 'Missing Pinecone API credentials'
1349 );
1350 }
1351
1352 try {
1353 // First, get all vector IDs by scanning Pinecone directly
1354 $all_vector_ids = array();
1355
1356 // Get fresh data from Pinecone
1357 $records = $this->mxchat_get_recent_1k_entries($pinecone_options);
1358 foreach ($records as $record) {
1359 if (!empty($record->id)) {
1360 $all_vector_ids[] = $record->id;
1361 }
1362 }
1363
1364 if (empty($all_vector_ids)) {
1365 return array(
1366 'success' => true,
1367 'message' => 'No vectors found to delete'
1368 );
1369 }
1370
1371 // Delete vectors in batches (Pinecone has limits on batch operations)
1372 $batch_size = 100;
1373 $batches = array_chunk($all_vector_ids, $batch_size);
1374 $deleted_count = 0;
1375 $failed_batches = 0;
1376
1377 foreach ($batches as $batch) {
1378 $result = $this->mxchat_delete_pinecone_batch($batch, $api_key, $host);
1379 if ($result['success']) {
1380 $deleted_count += count($batch);
1381 } else {
1382 $failed_batches++;
1383 }
1384 }
1385
1386 if ($failed_batches > 0) {
1387 return array(
1388 'success' => false,
1389 'message' => sprintf('Deleted %d vectors, but %d batches failed', $deleted_count, $failed_batches)
1390 );
1391 }
1392
1393 return array(
1394 'success' => true,
1395 'message' => "Successfully deleted {$deleted_count} vectors from Pinecone"
1396 );
1397
1398 } catch (Exception $e) {
1399 return array(
1400 'success' => false,
1401 'message' => $e->getMessage()
1402 );
1403 }
1404 }
1405
1406 /**
1407 * Deletes batch of vectors from Pinecone database
1408 */
1409 private function mxchat_delete_pinecone_batch($vector_ids, $api_key, $host) {
1410 // Build the API endpoint
1411 $api_endpoint = "https://{$host}/vectors/delete";
1412
1413 // Prepare the request body with the IDs
1414 $request_body = array(
1415 'ids' => $vector_ids
1416 );
1417
1418 // Make the deletion request
1419 $response = wp_remote_post($api_endpoint, array(
1420 'headers' => array(
1421 'Api-Key' => $api_key,
1422 'accept' => 'application/json',
1423 'content-type' => 'application/json'
1424 ),
1425 'body' => wp_json_encode($request_body),
1426 'timeout' => 60, // Increased timeout for batch operations
1427 'method' => 'POST'
1428 ));
1429
1430 // Handle WordPress HTTP API errors
1431 if (is_wp_error($response)) {
1432 return array(
1433 'success' => false,
1434 'message' => $response->get_error_message()
1435 );
1436 }
1437
1438 // Check response status
1439 $response_code = wp_remote_retrieve_response_code($response);
1440 $response_body = wp_remote_retrieve_body($response);
1441
1442 // Pinecone returns 200 for successful deletion
1443 if ($response_code !== 200) {
1444 //error_log('Pinecone batch deletion failed: HTTP ' . $response_code . ' - ' . $response_body);
1445 return array(
1446 'success' => false,
1447 'message' => sprintf(
1448 'Pinecone API error (HTTP %d): %s',
1449 $response_code,
1450 $response_body
1451 )
1452 );
1453 }
1454
1455 return array(
1456 'success' => true,
1457 'message' => 'Batch deleted successfully from Pinecone'
1458 );
1459 }
1460
1461
1462 /**
1463 * Deletes vector from Pinecone using API request
1464 */
1465 public function mxchat_delete_from_pinecone_by_vector_id($vector_id, $api_key, $host, $namespace = '') {
1466 //error_log('=== PINECONE DELETE OPERATION ===');
1467 //error_log('Vector ID: ' . $vector_id);
1468 //error_log('Host: ' . $host);
1469 //error_log('API Key: ' . (empty($api_key) ? 'EMPTY' : 'SET'));
1470
1471 // First, let's verify the vector exists before trying to delete
1472 $fetch_url = "https://{$host}/vectors/fetch";
1473
1474 $fetch_params = array(
1475 'ids' => array($vector_id)
1476 );
1477
1478 // Add namespace if provided (though you said you're not using namespaces)
1479 if (!empty($namespace)) {
1480 $fetch_params['namespace'] = $namespace;
1481 }
1482
1483 // Construct URL with query parameters for GET request
1484 $fetch_url_with_params = $fetch_url . '?' . http_build_query($fetch_params);
1485
1486 $fetch_response = wp_remote_get($fetch_url_with_params, array(
1487 'headers' => array(
1488 'Api-Key' => $api_key,
1489 'accept' => 'application/json'
1490 ),
1491 'timeout' => 15
1492 ));
1493
1494 if (!is_wp_error($fetch_response) && wp_remote_retrieve_response_code($fetch_response) === 200) {
1495 $fetch_body = wp_remote_retrieve_body($fetch_response);
1496 $fetch_data = json_decode($fetch_body, true);
1497
1498 //error_log('DEBUG: Fetch response: ' . print_r($fetch_data, true));
1499
1500 if (isset($fetch_data['vectors']) && isset($fetch_data['vectors'][$vector_id])) {
1501 //error_log('DEBUG: Vector EXISTS in this index before deletion');
1502 } else {
1503 //error_log('WARNING: Vector NOT FOUND in this index! It may be in a different bot\'s index');
1504 // You might want to return an error here
1505 }
1506 } else {
1507 //error_log('DEBUG: Could not fetch vector to verify existence');
1508 }
1509
1510 // Now proceed with deletion
1511 $api_endpoint = "https://{$host}/vectors/delete";
1512
1513 // Prepare the request body with the ID
1514 $request_body = array(
1515 'ids' => array($vector_id)
1516 );
1517
1518 // Add namespace if provided
1519 if (!empty($namespace)) {
1520 $request_body['namespace'] = $namespace;
1521 }
1522
1523 //error_log('DEBUG: Delete request body: ' . json_encode($request_body));
1524 //error_log('DEBUG: Delete endpoint: ' . $api_endpoint);
1525
1526 // Make the deletion request
1527 $response = wp_remote_post($api_endpoint, array(
1528 'headers' => array(
1529 'Api-Key' => $api_key,
1530 'accept' => 'application/json',
1531 'content-type' => 'application/json'
1532 ),
1533 'body' => wp_json_encode($request_body),
1534 'timeout' => 30
1535 ));
1536
1537 // Handle WordPress HTTP API errors
1538 if (is_wp_error($response)) {
1539 //error_log('DEBUG: WP Error: ' . $response->get_error_message());
1540 return array(
1541 'success' => false,
1542 'message' => $response->get_error_message()
1543 );
1544 }
1545
1546 // Check response status
1547 $response_code = wp_remote_retrieve_response_code($response);
1548 $response_body = wp_remote_retrieve_body($response);
1549
1550 //error_log('DEBUG: Delete response code: ' . $response_code);
1551 //error_log('DEBUG: Delete response body: ' . $response_body);
1552
1553 // Pinecone returns 200 for successful deletion (even if vector didn't exist)
1554 if ($response_code !== 200) {
1555 //error_log('DEBUG: Non-200 response from Pinecone');
1556 return array(
1557 'success' => false,
1558 'message' => sprintf(
1559 'Pinecone API error (HTTP %d): %s',
1560 $response_code,
1561 $response_body
1562 )
1563 );
1564 }
1565
1566 // After deletion, verify it's actually gone
1567 sleep(1); // Give Pinecone a moment to process
1568
1569 $verify_response = wp_remote_get($fetch_url_with_params, array(
1570 'headers' => array(
1571 'Api-Key' => $api_key,
1572 'accept' => 'application/json'
1573 ),
1574 'timeout' => 15
1575 ));
1576
1577 if (!is_wp_error($verify_response) && wp_remote_retrieve_response_code($verify_response) === 200) {
1578 $verify_body = wp_remote_retrieve_body($verify_response);
1579 $verify_data = json_decode($verify_body, true);
1580
1581 if (isset($verify_data['vectors']) && isset($verify_data['vectors'][$vector_id])) {
1582 //error_log('ERROR: Vector STILL EXISTS after deletion attempt!');
1583 return array(
1584 'success' => false,
1585 'message' => 'Vector still exists after deletion attempt'
1586 );
1587 } else {
1588 //error_log('SUCCESS: Vector confirmed deleted (or never existed)');
1589 }
1590 }
1591
1592 //error_log('=== END PINECONE DELETE OPERATION ===');
1593
1594 return array(
1595 'success' => true,
1596 'message' => 'Vector deleted successfully from Pinecone'
1597 );
1598 }
1599
1600 /**
1601 * Deletes data from Pinecone using a source URL
1602 */
1603 public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
1604 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1605 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1606
1607 if (empty($host) || empty($api_key)) {
1608 //error_log('MXChat: Pinecone deletion failed - missing configuration');
1609 return false;
1610 }
1611
1612 $api_endpoint = "https://{$host}/vectors/delete";
1613 $vector_id = md5($source_url);
1614
1615 $request_body = array(
1616 'ids' => array($vector_id)
1617 );
1618
1619 $response = wp_remote_post($api_endpoint, array(
1620 'headers' => array(
1621 'Api-Key' => $api_key,
1622 'accept' => 'application/json',
1623 'content-type' => 'application/json'
1624 ),
1625 'body' => wp_json_encode($request_body),
1626 'timeout' => 30
1627 ));
1628
1629 if (is_wp_error($response)) {
1630 //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
1631 return false;
1632 }
1633
1634 $response_code = wp_remote_retrieve_response_code($response);
1635 if ($response_code !== 200) {
1636 //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
1637 return false;
1638 }
1639
1640 return true;
1641 }
1642
1643
1644 /**
1645 * Deletes data from Pinecone index using API key
1646 */
1647 private function mxchat_delete_from_pinecone($urls, $api_key, $environment, $index_name) {
1648 // Get the Pinecone host from options (matching your store_in_pinecone_main pattern)
1649 $options = get_option('mxchat_pinecone_addon_options');
1650 $host = $options['mxchat_pinecone_host'] ?? '';
1651
1652 if (empty($host)) {
1653 return array(
1654 'success' => false,
1655 'message' => 'Pinecone host is not configured. Please set the host in your settings.'
1656 );
1657 }
1658
1659 // Build API endpoint using the configured host
1660 $api_endpoint = "https://{$host}/vectors/delete";
1661
1662 // Create vector IDs from URLs (matching your store method's ID generation)
1663 $vector_ids = array_map('md5', $urls);
1664
1665 // Prepare the delete request body
1666 $request_body = array(
1667 'ids' => $vector_ids,
1668 'filter' => array(
1669 'source_url' => array(
1670 '$in' => $urls
1671 )
1672 )
1673 );
1674
1675 // Make the deletion request
1676 $response = wp_remote_post($api_endpoint, array(
1677 'headers' => array(
1678 'Api-Key' => $api_key,
1679 'accept' => 'application/json',
1680 'content-type' => 'application/json'
1681 ),
1682 'body' => wp_json_encode($request_body),
1683 'timeout' => 30,
1684 'data_format' => 'body'
1685 ));
1686
1687 // Handle WordPress HTTP API errors
1688 if (is_wp_error($response)) {
1689 return array(
1690 'success' => false,
1691 'message' => $response->get_error_message()
1692 );
1693 }
1694
1695 // Check response status
1696 $response_code = wp_remote_retrieve_response_code($response);
1697 if ($response_code !== 200) {
1698 $body = wp_remote_retrieve_body($response);
1699 return array(
1700 'success' => false,
1701 'message' => sprintf(
1702 'Pinecone API error (HTTP %d): %s',
1703 $response_code,
1704 $body
1705 )
1706 );
1707 }
1708
1709 // Parse response body
1710 $body = wp_remote_retrieve_body($response);
1711 $response_data = json_decode($body, true);
1712
1713 // Final validation of the response
1714 if (json_last_error() !== JSON_ERROR_NONE) {
1715 return array(
1716 'success' => false,
1717 'message' => 'Failed to parse Pinecone response: ' . json_last_error_msg()
1718 );
1719 }
1720
1721 return array(
1722 'success' => true,
1723 'message' => sprintf('Successfully deleted %d vectors from Pinecone', count($vector_ids))
1724 );
1725 }
1726
1727
1728
1729 /**
1730 * Retrieves processed content from Pinecone API
1731 *
1732 * @param array $pinecone_options Pinecone configuration options
1733 * @param array $post_ids Optional array of specific post IDs to check (if empty, checks all)
1734 * @return array Processed data keyed by post ID
1735 */
1736 public function mxchat_get_pinecone_processed_content($pinecone_options, $post_ids = array()) {
1737 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1738 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1739
1740 if (empty($api_key) || empty($host)) {
1741 return array();
1742 }
1743
1744 $pinecone_data = array();
1745
1746 try {
1747 // Always get fresh data from Pinecone
1748 $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options, $post_ids);
1749
1750 // Method 2: Final fallback - try stats endpoint (if available)
1751 if (empty($pinecone_data)) {
1752 $stats_url = "https://{$host}/describe_index_stats";
1753
1754 $response = wp_remote_post($stats_url, array(
1755 'headers' => array(
1756 'Api-Key' => $api_key,
1757 'Content-Type' => 'application/json'
1758 ),
1759 'body' => json_encode(array()),
1760 'timeout' => 30
1761 ));
1762
1763 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1764 $body = wp_remote_retrieve_body($response);
1765 $stats_data = json_decode($body, true);
1766 }
1767 }
1768
1769 } catch (Exception $e) {
1770 // Log error but don't return cached data
1771 }
1772
1773 return $pinecone_data;
1774 }
1775 // ========================================
1776 // HELPER METHODS
1777 // ========================================
1778
1779 /**
1780 * Validates Pinecone API credentials
1781 */
1782 private function mxchat_validate_pinecone_credentials($api_key, $host) {
1783 if (empty($api_key) || empty($host)) {
1784 return false;
1785 }
1786 return true;
1787 }
1788
1789 /**
1790 * Get Pinecone API credentials from options
1791 */
1792 private function mxchat_get_pinecone_credentials() {
1793 $options = get_option('mxchat_options', array());
1794 return array(
1795 'api_key' => isset($options['pinecone_api_key']) ? $options['pinecone_api_key'] : '',
1796 'host' => isset($options['pinecone_host']) ? $options['pinecone_host'] : ''
1797 );
1798 }
1799
1800 /**
1801 * Log Pinecone operation errors
1802 */
1803 private function log_pinecone_error($operation, $error_message) {
1804 //error_log("MxChat Pinecone {$operation} Error: " . $error_message);
1805 }
1806
1807 // ========================================
1808 // STATIC ACCESS METHODS (for backward compatibility)
1809 // ========================================
1810
1811 /**
1812 * Get singleton instance
1813 */
1814 public static function get_instance() {
1815 static $instance = null;
1816 if ($instance === null) {
1817 $instance = new self();
1818 }
1819 return $instance;
1820 }
1821 }
1822
1823 // Initialize the Pinecone manager
1824 $mxchat_pinecone_manager = MxChat_Pinecone_Manager::get_instance();
1825