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

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