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

1,897 lines 68.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * File: admin/class-pinecone-manager.php
4 *
5 * Handles all Pinecone vector database operations for MxChat
6 */
7
8 if (!defined('ABSPATH')) {
9 exit; // Exit if accessed directly
10 }
11
12 class MxChat_Pinecone_Manager {
13
14 /**
15 * Constructor
16 */
17 public function __construct() {
18 // Hook into WordPress actions if needed
19 add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1);
20 }
21
22 // ========================================
23 // PINECONE FETCH OPERATIONS
24 // ========================================
25
26
27 /**
28 * Fetches 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 return array('data' => array(), 'total' => 0, 'total_in_database' => 0, 'showing_recent_only' => false);
130 }
131 }
132
133 /**
134 * Optimized fetch for large Pinecone databases
135 * ADDED 2.6.1: Prevents crashes with large datasets
136 * UPDATED 2.6.1: When searching, uses semantic search with embedded query for accurate results across all 13K+ records
137 */
138 private function mxchat_fetch_pinecone_page_optimized($pinecone_options, $search_query = '', $page = 1, $per_page = 20, $bot_id = 'default', $content_type = '') {
139 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
140 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
141 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
142
143 try {
144 // If user is searching, use semantic search with embedded query
145 // This searches ALL records in Pinecone, not just fetched ones
146 if (!empty($search_query)) {
147 return $this->mxchat_semantic_search_pinecone($pinecone_options, $search_query, $page, $per_page, $bot_id, $content_type);
148 }
149
150 // For browsing (no search), use Pinecone's list endpoint for true pagination
151 return $this->mxchat_list_pinecone_records($pinecone_options, $page, $per_page, $bot_id, $content_type);
152
153 } catch (Exception $e) {
154 error_log('MxChat optimized fetch exception: ' . $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 return array();
931 }
932 }
933 /**
934 * Get embedding dimensions based on the selected model
935 * ADD THIS NEW FUNCTION
936 */
937 private function mxchat_get_embedding_dimensions() {
938 $options = get_option('mxchat_options', array());
939 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
940
941 // Define dimensions for different models
942 $model_dimensions = array(
943 'text-embedding-ada-002' => 1536,
944 'text-embedding-3-small' => 1536,
945 'text-embedding-3-large' => 3072,
946 'voyage-2' => 1024,
947 'voyage-large-2' => 1536,
948 'voyage-3-large' => 2048,
949 'gemini-embedding-001' => 1536,
950 );
951
952 // Check if it's a voyage model with custom dimensions
953 if (strpos($selected_model, 'voyage-3-large') === 0) {
954 $custom_dimensions = $options['voyage_output_dimension'] ?? 2048;
955 return intval($custom_dimensions);
956 }
957
958 // Check if it's a gemini model with custom dimensions
959 if (strpos($selected_model, 'gemini-embedding') === 0) {
960 $custom_dimensions = $options['gemini_output_dimension'] ?? 1536;
961 return intval($custom_dimensions);
962 }
963
964 // Return known dimensions or default to 1536
965 return $model_dimensions[$selected_model] ?? 1536;
966 }
967
968 /**
969 * Generate random unit vector with correct dimensions
970 * ADD THIS NEW FUNCTION
971 */
972 private function mxchat_generate_random_vector() {
973 $dimensions = $this->mxchat_get_embedding_dimensions();
974
975 $random_vector = array();
976 for ($i = 0; $i < $dimensions; $i++) {
977 $random_vector[] = (rand(-1000, 1000) / 1000.0);
978 }
979
980 // Normalize the vector to unit length
981 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $random_vector)));
982 if ($magnitude > 0) {
983 $random_vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $random_vector);
984 }
985
986 return $random_vector;
987 }
988
989
990 /**
991 * Get recent entries from Pinecone
992 * UPDATED 2.6.1: Now uses safe version with memory limits to prevent crashes
993 * @deprecated Use mxchat_get_recent_entries_safe() instead for new code
994 */
995 private function mxchat_get_recent_1k_entries($pinecone_options, $bot_id = 'default') {
996 // Delegate to the safe version with a reasonable limit
997 // This prevents crashes with large datasets (13K+ products)
998 return $this->mxchat_get_recent_entries_safe($pinecone_options, $bot_id, 500);
999 }
1000 /**
1001 * Generate fixed query vectors for consistent results
1002 */
1003 private function mxchat_generate_fixed_query_vectors() {
1004 $dimensions = $this->mxchat_get_embedding_dimensions();
1005 $vectors = array();
1006
1007 // Create 5 fixed vectors with different patterns for better coverage
1008 $patterns = array(
1009 'zeros_with_ones' => 0.1, // Mostly zeros with some 1s
1010 'ascending' => 0.2, // Ascending pattern
1011 'descending' => 0.3, // Descending pattern
1012 'alternating' => 0.4, // Alternating positive/negative
1013 'center_weighted' => 0.5 // Higher values in center
1014 );
1015
1016 foreach ($patterns as $pattern_name => $seed) {
1017 $vector = array();
1018
1019 for ($i = 0; $i < $dimensions; $i++) {
1020 switch ($pattern_name) {
1021 case 'zeros_with_ones':
1022 $vector[] = ($i % 10 === 0) ? 1.0 : 0.0;
1023 break;
1024 case 'ascending':
1025 $vector[] = ($i / $dimensions) * 2 - 1; // Range -1 to 1
1026 break;
1027 case 'descending':
1028 $vector[] = (($dimensions - $i) / $dimensions) * 2 - 1;
1029 break;
1030 case 'alternating':
1031 $vector[] = ($i % 2 === 0) ? $seed : -$seed;
1032 break;
1033 case 'center_weighted':
1034 $center = $dimensions / 2;
1035 $distance = abs($i - $center) / $center;
1036 $vector[] = (1 - $distance) * $seed;
1037 break;
1038 }
1039 }
1040
1041 // Normalize the vector to unit length
1042 $magnitude = sqrt(array_sum(array_map(function($x) { return $x * $x; }, $vector)));
1043 if ($magnitude > 0) {
1044 $vector = array_map(function($x) use ($magnitude) { return $x / $magnitude; }, $vector);
1045 }
1046
1047 $vectors[] = $vector;
1048 }
1049
1050 return $vectors;
1051 }
1052
1053
1054 /**
1055 * Scan Pinecone for processed content
1056 * UPDATED 2.6.2: Uses direct ID lookup via fetch API instead of random vector scanning
1057 * This removes the 10K record limit and scales to any database size
1058 *
1059 * @param array $pinecone_options Pinecone configuration options
1060 * @param array $post_ids Optional array of specific post IDs to check (if empty, checks all published posts)
1061 * @return array Processed data keyed by post ID
1062 */
1063 public function mxchat_scan_pinecone_for_processed_content($pinecone_options, $post_ids = array()) {
1064 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1065 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1066
1067 if (empty($api_key) || empty($host)) {
1068 return array();
1069 }
1070
1071 try {
1072 // If no specific post IDs provided, get all published posts
1073 if (empty($post_ids)) {
1074 $posts = get_posts(array(
1075 'post_type' => 'any',
1076 'post_status' => 'publish',
1077 'posts_per_page' => -1,
1078 'fields' => 'ids',
1079 'no_found_rows' => true,
1080 'update_post_meta_cache' => false,
1081 'update_post_term_cache' => false,
1082 ));
1083 $post_ids = $posts;
1084 }
1085
1086 if (empty($post_ids)) {
1087 return array();
1088 }
1089
1090 // Build a map of vector_id => post data for lookup
1091 $vector_id_map = array();
1092 foreach ($post_ids as $post_id) {
1093 $permalink = get_permalink($post_id);
1094 if ($permalink) {
1095 $vector_id = md5($permalink);
1096 $vector_id_map[$vector_id] = array(
1097 'post_id' => $post_id,
1098 'url' => $permalink
1099 );
1100 }
1101 }
1102
1103 if (empty($vector_id_map)) {
1104 return array();
1105 }
1106
1107 // Batch check Pinecone using fetch API (max 1000 IDs per request)
1108 $all_vector_ids = array_keys($vector_id_map);
1109 $chunks = array_chunk($all_vector_ids, 1000);
1110 $processed_data = array();
1111
1112 foreach ($chunks as $chunk) {
1113 $fetch_url = "https://{$host}/vectors/fetch";
1114
1115 $response = wp_remote_post($fetch_url, array(
1116 'headers' => array(
1117 'Api-Key' => $api_key,
1118 'Content-Type' => 'application/json'
1119 ),
1120 'body' => json_encode(array('ids' => $chunk)),
1121 'timeout' => 30
1122 ));
1123
1124 if (is_wp_error($response)) {
1125 continue;
1126 }
1127
1128 $response_code = wp_remote_retrieve_response_code($response);
1129 if ($response_code !== 200) {
1130 continue;
1131 }
1132
1133 $body = wp_remote_retrieve_body($response);
1134 $data = json_decode($body, true);
1135
1136 // Process returned vectors
1137 if (isset($data['vectors']) && is_array($data['vectors'])) {
1138 foreach ($data['vectors'] as $vector_id => $vector_data) {
1139 if (isset($vector_id_map[$vector_id])) {
1140 $post_info = $vector_id_map[$vector_id];
1141 $post_id = $post_info['post_id'];
1142 $metadata = $vector_data['metadata'] ?? array();
1143
1144 $created_at = $metadata['created_at'] ?? '';
1145 $processed_date = 'Recently';
1146 $timestamp = current_time('timestamp');
1147
1148 if (!empty($created_at)) {
1149 $ts = is_numeric($created_at) ? $created_at : strtotime($created_at);
1150 if ($ts) {
1151 $timestamp = $ts;
1152 $processed_date = human_time_diff($ts, current_time('timestamp')) . ' ago';
1153 }
1154 }
1155
1156 $processed_data[$post_id] = array(
1157 'db_id' => $vector_id,
1158 'processed_date' => $processed_date,
1159 'url' => $post_info['url'],
1160 'source' => 'pinecone',
1161 'timestamp' => $timestamp
1162 );
1163 }
1164 }
1165 }
1166 }
1167
1168 return $processed_data;
1169
1170 } catch (Exception $e) {
1171 return array();
1172 }
1173 }
1174
1175 /**
1176 * Get total count from Pinecone stats API
1177 * UPDATED: Removed cache fallback reference
1178 */
1179 private function mxchat_get_pinecone_total_count($pinecone_options, $bot_id = 'default') {
1180 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1181 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1182 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
1183
1184 if (empty($api_key) || empty($host)) {
1185 return 0;
1186 }
1187
1188 try {
1189 $stats_url = "https://{$host}/describe_index_stats";
1190
1191 // describe_index_stats doesn't need a body, just the POST request
1192 $response = wp_remote_post($stats_url, array(
1193 'headers' => array(
1194 'Api-Key' => $api_key,
1195 'Content-Type' => 'application/json'
1196 ),
1197 'body' => '{}',
1198 'timeout' => 15
1199 ));
1200
1201 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1202 $body = wp_remote_retrieve_body($response);
1203 $stats_data = json_decode($body, true);
1204
1205 // If namespace is specified, get count from that specific namespace
1206 // Pinecone stats response format: { namespaces: { "ns": { vectorCount: N } }, totalVectorCount: N }
1207 if (!empty($namespace) && isset($stats_data['namespaces'][$namespace]['vectorCount'])) {
1208 $namespace_count = intval($stats_data['namespaces'][$namespace]['vectorCount']);
1209 //error_log('DEBUG: Got namespace-specific count: ' . $namespace_count . ' for namespace: ' . $namespace);
1210 return $namespace_count;
1211 }
1212
1213 // If no namespace specified or namespace not found in response, use total
1214 $total_count = $stats_data['totalVectorCount'] ?? 0;
1215 if ($total_count > 0) {
1216 //error_log('DEBUG: Got total count from stats API: ' . $total_count);
1217 return intval($total_count);
1218 }
1219 }
1220
1221 // If stats API fails, return 0 instead of using cache
1222 return 0;
1223
1224 } catch (Exception $e) {
1225 //error_log('DEBUG: Exception getting total count: ' . $e->getMessage());
1226 return 0;
1227 }
1228 }
1229
1230 /**
1231 * Get bot-specific Pinecone configuration for database operations
1232 */
1233 public function mxchat_get_bot_pinecone_options($bot_id = 'default') {
1234 //error_log('DEBUG: Getting Pinecone options for bot: ' . $bot_id);
1235
1236 // If default bot or multi-bot add-on not active, use default Pinecone config
1237 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1238 $addon_options = get_option('mxchat_pinecone_addon_options', array());
1239 //error_log('DEBUG: Using default Pinecone options');
1240 return $addon_options;
1241 }
1242
1243 // Get bot-specific configuration using the filter
1244 $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1245
1246 //error_log('DEBUG: Bot config from filter: ' . print_r($bot_config, true));
1247
1248 // Check if we got valid bot-specific config
1249 if (!empty($bot_config) && isset($bot_config['use_pinecone']) && $bot_config['use_pinecone']) {
1250 // Convert bot config to the format expected by fetch functions
1251 $pinecone_options = array(
1252 'mxchat_use_pinecone' => '1',
1253 'mxchat_pinecone_api_key' => $bot_config['api_key'] ?? '',
1254 'mxchat_pinecone_host' => $bot_config['host'] ?? '',
1255 'mxchat_pinecone_namespace' => $bot_config['namespace'] ?? '',
1256 'mxchat_pinecone_environment' => '',
1257 'mxchat_pinecone_index' => ''
1258 );
1259
1260 //error_log('DEBUG: Returning bot-specific Pinecone options for bot: ' . $bot_id);
1261 return $pinecone_options;
1262 }
1263
1264 // Fallback to default options if bot-specific config is invalid
1265 //error_log('DEBUG: Bot-specific config invalid, falling back to default');
1266 return get_option('mxchat_pinecone_addon_options', array());
1267 }
1268
1269 /**
1270 * Get bot-specific Pinecone configuration
1271 * Used in the knowledge retrieval functions
1272 */
1273 private function get_bot_pinecone_config($bot_id = 'default') {
1274 //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1275
1276 // If default bot or multi-bot add-on not active, use default Pinecone config
1277 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1278 //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1279 $addon_options = get_option('mxchat_pinecone_addon_options', array());
1280 $config = array(
1281 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
1282 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
1283 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
1284 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
1285 );
1286 //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1287 return $config;
1288 }
1289
1290 //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1291
1292 // Hook for multi-bot add-on to provide bot-specific Pinecone config
1293 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1294
1295 if (!empty($bot_pinecone_config)) {
1296 //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1297 //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1298 //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1299 //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1300 } else {
1301 //error_log("MXCHAT DEBUG: Filter returned empty config!");
1302 }
1303
1304 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1305 }
1306
1307
1308 /**
1309 * Fetches vectors from Pinecone using provided IDs (for content selection feature)
1310 */
1311 public function fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
1312 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1313 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1314
1315 if (empty($api_key) || empty($host) || empty($vector_ids)) {
1316 return array();
1317 }
1318
1319 try {
1320 $fetch_url = "https://{$host}/vectors/fetch";
1321
1322 // Pinecone fetch API allows fetching specific vectors by ID
1323 $fetch_data = array(
1324 'ids' => array_values($vector_ids)
1325 );
1326
1327 $response = wp_remote_post($fetch_url, array(
1328 'headers' => array(
1329 'Api-Key' => $api_key,
1330 'Content-Type' => 'application/json'
1331 ),
1332 'body' => json_encode($fetch_data),
1333 'timeout' => 30
1334 ));
1335
1336 if (is_wp_error($response)) {
1337 return array();
1338 }
1339
1340 $response_code = wp_remote_retrieve_response_code($response);
1341
1342 if ($response_code !== 200) {
1343 return array();
1344 }
1345
1346 $body = wp_remote_retrieve_body($response);
1347 $data = json_decode($body, true);
1348
1349 if (!isset($data['vectors'])) {
1350 return array();
1351 }
1352
1353 $processed_data = array();
1354
1355 foreach ($data['vectors'] as $vector_id => $vector_data) {
1356 $metadata = $vector_data['metadata'] ?? array();
1357 $source_url = $metadata['source_url'] ?? '';
1358
1359 if (!empty($source_url)) {
1360 $post_id = url_to_postid($source_url);
1361 if ($post_id) {
1362 $created_at = $metadata['created_at'] ?? '';
1363 $processed_date = 'Recently'; // Default
1364
1365 if (!empty($created_at)) {
1366 $timestamp = is_numeric($created_at) ? $created_at : strtotime($created_at);
1367 if ($timestamp) {
1368 $processed_date = human_time_diff($timestamp, current_time('timestamp')) . ' ago';
1369 }
1370 }
1371
1372 $processed_data[$post_id] = array(
1373 'db_id' => $vector_id,
1374 'processed_date' => $processed_date,
1375 'url' => $source_url,
1376 'source' => 'pinecone',
1377 'timestamp' => $timestamp ?? current_time('timestamp')
1378 );
1379 }
1380 }
1381 }
1382
1383 return $processed_data;
1384
1385 } catch (Exception $e) {
1386 return array();
1387 }
1388 }
1389 // ========================================
1390 // PINECONE DELETE OPERATIONS
1391 // ========================================
1392
1393 /**
1394 * Delete all vectors from Pinecone
1395 * Loops until all vectors are deleted (handles large databases)
1396 */
1397 public function mxchat_delete_all_from_pinecone($pinecone_options) {
1398 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1399 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1400
1401 if (empty($api_key) || empty($host)) {
1402 return array(
1403 'success' => false,
1404 'message' => 'Missing Pinecone API credentials'
1405 );
1406 }
1407
1408 try {
1409 $total_deleted = 0;
1410 $failed_batches = 0;
1411 $max_iterations = 100; // Safety limit to prevent infinite loops
1412 $iteration = 0;
1413
1414 // Loop until no more vectors are found
1415 do {
1416 $iteration++;
1417
1418 // Get a batch of vector IDs from Pinecone
1419 $records = $this->mxchat_get_recent_1k_entries($pinecone_options);
1420 $vector_ids = array();
1421
1422 foreach ($records as $record) {
1423 if (!empty($record->id)) {
1424 $vector_ids[] = $record->id;
1425 }
1426 }
1427
1428 // If no vectors found, we're done
1429 if (empty($vector_ids)) {
1430 break;
1431 }
1432
1433 // Delete vectors in batches (Pinecone has limits on batch operations)
1434 $batch_size = 100;
1435 $batches = array_chunk($vector_ids, $batch_size);
1436
1437 foreach ($batches as $batch) {
1438 $result = $this->mxchat_delete_pinecone_batch($batch, $api_key, $host);
1439 if ($result['success']) {
1440 $total_deleted += count($batch);
1441 } else {
1442 $failed_batches++;
1443 }
1444 }
1445
1446 // Small delay to avoid rate limiting
1447 usleep(100000); // 100ms
1448
1449 } while ($iteration < $max_iterations);
1450
1451 if ($total_deleted === 0) {
1452 return array(
1453 'success' => true,
1454 'message' => 'No vectors found to delete'
1455 );
1456 }
1457
1458 if ($failed_batches > 0) {
1459 return array(
1460 'success' => false,
1461 'message' => sprintf('Deleted %d vectors, but %d batches failed', $total_deleted, $failed_batches)
1462 );
1463 }
1464
1465 return array(
1466 'success' => true,
1467 'message' => "Successfully deleted {$total_deleted} vectors from Pinecone"
1468 );
1469
1470 } catch (Exception $e) {
1471 return array(
1472 'success' => false,
1473 'message' => $e->getMessage()
1474 );
1475 }
1476 }
1477
1478 /**
1479 * Deletes batch of vectors from Pinecone database
1480 */
1481 private function mxchat_delete_pinecone_batch($vector_ids, $api_key, $host) {
1482 // Build the API endpoint
1483 $api_endpoint = "https://{$host}/vectors/delete";
1484
1485 // Prepare the request body with the IDs
1486 $request_body = array(
1487 'ids' => $vector_ids
1488 );
1489
1490 // Make the deletion request
1491 $response = wp_remote_post($api_endpoint, array(
1492 'headers' => array(
1493 'Api-Key' => $api_key,
1494 'accept' => 'application/json',
1495 'content-type' => 'application/json'
1496 ),
1497 'body' => wp_json_encode($request_body),
1498 'timeout' => 60, // Increased timeout for batch operations
1499 'method' => 'POST'
1500 ));
1501
1502 // Handle WordPress HTTP API errors
1503 if (is_wp_error($response)) {
1504 return array(
1505 'success' => false,
1506 'message' => $response->get_error_message()
1507 );
1508 }
1509
1510 // Check response status
1511 $response_code = wp_remote_retrieve_response_code($response);
1512 $response_body = wp_remote_retrieve_body($response);
1513
1514 // Pinecone returns 200 for successful deletion
1515 if ($response_code !== 200) {
1516 //error_log('Pinecone batch deletion failed: HTTP ' . $response_code . ' - ' . $response_body);
1517 return array(
1518 'success' => false,
1519 'message' => sprintf(
1520 'Pinecone API error (HTTP %d): %s',
1521 $response_code,
1522 $response_body
1523 )
1524 );
1525 }
1526
1527 return array(
1528 'success' => true,
1529 'message' => 'Batch deleted successfully from Pinecone'
1530 );
1531 }
1532
1533
1534 /**
1535 * Deletes vector from Pinecone using API request
1536 */
1537 public function mxchat_delete_from_pinecone_by_vector_id($vector_id, $api_key, $host, $namespace = '') {
1538 //error_log('=== PINECONE DELETE OPERATION ===');
1539 //error_log('Vector ID: ' . $vector_id);
1540 //error_log('Host: ' . $host);
1541 //error_log('API Key: ' . (empty($api_key) ? 'EMPTY' : 'SET'));
1542
1543 // First, let's verify the vector exists before trying to delete
1544 $fetch_url = "https://{$host}/vectors/fetch";
1545
1546 $fetch_params = array(
1547 'ids' => array($vector_id)
1548 );
1549
1550 // Add namespace if provided (though you said you're not using namespaces)
1551 if (!empty($namespace)) {
1552 $fetch_params['namespace'] = $namespace;
1553 }
1554
1555 // Construct URL with query parameters for GET request
1556 $fetch_url_with_params = $fetch_url . '?' . http_build_query($fetch_params);
1557
1558 $fetch_response = wp_remote_get($fetch_url_with_params, array(
1559 'headers' => array(
1560 'Api-Key' => $api_key,
1561 'accept' => 'application/json'
1562 ),
1563 'timeout' => 15
1564 ));
1565
1566 if (!is_wp_error($fetch_response) && wp_remote_retrieve_response_code($fetch_response) === 200) {
1567 $fetch_body = wp_remote_retrieve_body($fetch_response);
1568 $fetch_data = json_decode($fetch_body, true);
1569
1570 //error_log('DEBUG: Fetch response: ' . print_r($fetch_data, true));
1571
1572 if (isset($fetch_data['vectors']) && isset($fetch_data['vectors'][$vector_id])) {
1573 //error_log('DEBUG: Vector EXISTS in this index before deletion');
1574 } else {
1575 //error_log('WARNING: Vector NOT FOUND in this index! It may be in a different bot\'s index');
1576 // You might want to return an error here
1577 }
1578 } else {
1579 //error_log('DEBUG: Could not fetch vector to verify existence');
1580 }
1581
1582 // Now proceed with deletion
1583 $api_endpoint = "https://{$host}/vectors/delete";
1584
1585 // Prepare the request body with the ID
1586 $request_body = array(
1587 'ids' => array($vector_id)
1588 );
1589
1590 // Add namespace if provided
1591 if (!empty($namespace)) {
1592 $request_body['namespace'] = $namespace;
1593 }
1594
1595 //error_log('DEBUG: Delete request body: ' . json_encode($request_body));
1596 //error_log('DEBUG: Delete endpoint: ' . $api_endpoint);
1597
1598 // Make the deletion request
1599 $response = wp_remote_post($api_endpoint, array(
1600 'headers' => array(
1601 'Api-Key' => $api_key,
1602 'accept' => 'application/json',
1603 'content-type' => 'application/json'
1604 ),
1605 'body' => wp_json_encode($request_body),
1606 'timeout' => 30
1607 ));
1608
1609 // Handle WordPress HTTP API errors
1610 if (is_wp_error($response)) {
1611 //error_log('DEBUG: WP Error: ' . $response->get_error_message());
1612 return array(
1613 'success' => false,
1614 'message' => $response->get_error_message()
1615 );
1616 }
1617
1618 // Check response status
1619 $response_code = wp_remote_retrieve_response_code($response);
1620 $response_body = wp_remote_retrieve_body($response);
1621
1622 //error_log('DEBUG: Delete response code: ' . $response_code);
1623 //error_log('DEBUG: Delete response body: ' . $response_body);
1624
1625 // Pinecone returns 200 for successful deletion (even if vector didn't exist)
1626 if ($response_code !== 200) {
1627 //error_log('DEBUG: Non-200 response from Pinecone');
1628 return array(
1629 'success' => false,
1630 'message' => sprintf(
1631 'Pinecone API error (HTTP %d): %s',
1632 $response_code,
1633 $response_body
1634 )
1635 );
1636 }
1637
1638 // After deletion, verify it's actually gone
1639 sleep(1); // Give Pinecone a moment to process
1640
1641 $verify_response = wp_remote_get($fetch_url_with_params, array(
1642 'headers' => array(
1643 'Api-Key' => $api_key,
1644 'accept' => 'application/json'
1645 ),
1646 'timeout' => 15
1647 ));
1648
1649 if (!is_wp_error($verify_response) && wp_remote_retrieve_response_code($verify_response) === 200) {
1650 $verify_body = wp_remote_retrieve_body($verify_response);
1651 $verify_data = json_decode($verify_body, true);
1652
1653 if (isset($verify_data['vectors']) && isset($verify_data['vectors'][$vector_id])) {
1654 //error_log('ERROR: Vector STILL EXISTS after deletion attempt!');
1655 return array(
1656 'success' => false,
1657 'message' => 'Vector still exists after deletion attempt'
1658 );
1659 } else {
1660 //error_log('SUCCESS: Vector confirmed deleted (or never existed)');
1661 }
1662 }
1663
1664 //error_log('=== END PINECONE DELETE OPERATION ===');
1665
1666 return array(
1667 'success' => true,
1668 'message' => 'Vector deleted successfully from Pinecone'
1669 );
1670 }
1671
1672 /**
1673 * Deletes data from Pinecone using a source URL
1674 */
1675 public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
1676 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1677 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1678
1679 if (empty($host) || empty($api_key)) {
1680 //error_log('MXChat: Pinecone deletion failed - missing configuration');
1681 return false;
1682 }
1683
1684 $api_endpoint = "https://{$host}/vectors/delete";
1685 $vector_id = md5($source_url);
1686
1687 $request_body = array(
1688 'ids' => array($vector_id)
1689 );
1690
1691 $response = wp_remote_post($api_endpoint, array(
1692 'headers' => array(
1693 'Api-Key' => $api_key,
1694 'accept' => 'application/json',
1695 'content-type' => 'application/json'
1696 ),
1697 'body' => wp_json_encode($request_body),
1698 'timeout' => 30
1699 ));
1700
1701 if (is_wp_error($response)) {
1702 //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
1703 return false;
1704 }
1705
1706 $response_code = wp_remote_retrieve_response_code($response);
1707 if ($response_code !== 200) {
1708 //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
1709 return false;
1710 }
1711
1712 return true;
1713 }
1714
1715
1716 /**
1717 * Deletes data from Pinecone index using API key
1718 */
1719 private function mxchat_delete_from_pinecone($urls, $api_key, $environment, $index_name) {
1720 // Get the Pinecone host from options (matching your store_in_pinecone_main pattern)
1721 $options = get_option('mxchat_pinecone_addon_options');
1722 $host = $options['mxchat_pinecone_host'] ?? '';
1723
1724 if (empty($host)) {
1725 return array(
1726 'success' => false,
1727 'message' => 'Pinecone host is not configured. Please set the host in your settings.'
1728 );
1729 }
1730
1731 // Build API endpoint using the configured host
1732 $api_endpoint = "https://{$host}/vectors/delete";
1733
1734 // Create vector IDs from URLs (matching your store method's ID generation)
1735 $vector_ids = array_map('md5', $urls);
1736
1737 // Prepare the delete request body
1738 $request_body = array(
1739 'ids' => $vector_ids,
1740 'filter' => array(
1741 'source_url' => array(
1742 '$in' => $urls
1743 )
1744 )
1745 );
1746
1747 // Make the deletion request
1748 $response = wp_remote_post($api_endpoint, array(
1749 'headers' => array(
1750 'Api-Key' => $api_key,
1751 'accept' => 'application/json',
1752 'content-type' => 'application/json'
1753 ),
1754 'body' => wp_json_encode($request_body),
1755 'timeout' => 30,
1756 'data_format' => 'body'
1757 ));
1758
1759 // Handle WordPress HTTP API errors
1760 if (is_wp_error($response)) {
1761 return array(
1762 'success' => false,
1763 'message' => $response->get_error_message()
1764 );
1765 }
1766
1767 // Check response status
1768 $response_code = wp_remote_retrieve_response_code($response);
1769 if ($response_code !== 200) {
1770 $body = wp_remote_retrieve_body($response);
1771 return array(
1772 'success' => false,
1773 'message' => sprintf(
1774 'Pinecone API error (HTTP %d): %s',
1775 $response_code,
1776 $body
1777 )
1778 );
1779 }
1780
1781 // Parse response body
1782 $body = wp_remote_retrieve_body($response);
1783 $response_data = json_decode($body, true);
1784
1785 // Final validation of the response
1786 if (json_last_error() !== JSON_ERROR_NONE) {
1787 return array(
1788 'success' => false,
1789 'message' => 'Failed to parse Pinecone response: ' . json_last_error_msg()
1790 );
1791 }
1792
1793 return array(
1794 'success' => true,
1795 'message' => sprintf('Successfully deleted %d vectors from Pinecone', count($vector_ids))
1796 );
1797 }
1798
1799
1800
1801 /**
1802 * Retrieves processed content from Pinecone API
1803 *
1804 * @param array $pinecone_options Pinecone configuration options
1805 * @param array $post_ids Optional array of specific post IDs to check (if empty, checks all)
1806 * @return array Processed data keyed by post ID
1807 */
1808 public function mxchat_get_pinecone_processed_content($pinecone_options, $post_ids = array()) {
1809 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1810 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1811
1812 if (empty($api_key) || empty($host)) {
1813 return array();
1814 }
1815
1816 $pinecone_data = array();
1817
1818 try {
1819 // Always get fresh data from Pinecone
1820 $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options, $post_ids);
1821
1822 // Method 2: Final fallback - try stats endpoint (if available)
1823 if (empty($pinecone_data)) {
1824 $stats_url = "https://{$host}/describe_index_stats";
1825
1826 $response = wp_remote_post($stats_url, array(
1827 'headers' => array(
1828 'Api-Key' => $api_key,
1829 'Content-Type' => 'application/json'
1830 ),
1831 'body' => json_encode(array()),
1832 'timeout' => 30
1833 ));
1834
1835 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1836 $body = wp_remote_retrieve_body($response);
1837 $stats_data = json_decode($body, true);
1838 }
1839 }
1840
1841 } catch (Exception $e) {
1842 // Log error but don't return cached data
1843 }
1844
1845 return $pinecone_data;
1846 }
1847 // ========================================
1848 // HELPER METHODS
1849 // ========================================
1850
1851 /**
1852 * Validates Pinecone API credentials
1853 */
1854 private function mxchat_validate_pinecone_credentials($api_key, $host) {
1855 if (empty($api_key) || empty($host)) {
1856 return false;
1857 }
1858 return true;
1859 }
1860
1861 /**
1862 * Get Pinecone API credentials from options
1863 */
1864 private function mxchat_get_pinecone_credentials() {
1865 $options = get_option('mxchat_options', array());
1866 return array(
1867 'api_key' => isset($options['pinecone_api_key']) ? $options['pinecone_api_key'] : '',
1868 'host' => isset($options['pinecone_host']) ? $options['pinecone_host'] : ''
1869 );
1870 }
1871
1872 /**
1873 * Log Pinecone operation errors
1874 */
1875 private function log_pinecone_error($operation, $error_message) {
1876 //error_log("MxChat Pinecone {$operation} Error: " . $error_message);
1877 }
1878
1879 // ========================================
1880 // STATIC ACCESS METHODS (for backward compatibility)
1881 // ========================================
1882
1883 /**
1884 * Get singleton instance
1885 */
1886 public static function get_instance() {
1887 static $instance = null;
1888 if ($instance === null) {
1889 $instance = new self();
1890 }
1891 return $instance;
1892 }
1893 }
1894
1895 // Initialize the Pinecone manager
1896 $mxchat_pinecone_manager = MxChat_Pinecone_Manager::get_instance();
1897