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

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