PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.6.3
MxChat – AI Chatbot & Content Generation for WordPress v2.6.3
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / admin / class-pinecone-manager.php

class-pinecone-manager.php in MxChat – AI Chatbot & Content Generation for WordPress 2.6.3, at admin/class-pinecone-manager.php

1,880 lines 68.3 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 */
1396 public function mxchat_delete_all_from_pinecone($pinecone_options) {
1397 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1398 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1399
1400 if (empty($api_key) || empty($host)) {
1401 return array(
1402 'success' => false,
1403 'message' => 'Missing Pinecone API credentials'
1404 );
1405 }
1406
1407 try {
1408 // First, get all vector IDs by scanning Pinecone directly
1409 $all_vector_ids = array();
1410
1411 // Get fresh data from Pinecone
1412 $records = $this->mxchat_get_recent_1k_entries($pinecone_options);
1413 foreach ($records as $record) {
1414 if (!empty($record->id)) {
1415 $all_vector_ids[] = $record->id;
1416 }
1417 }
1418
1419 if (empty($all_vector_ids)) {
1420 return array(
1421 'success' => true,
1422 'message' => 'No vectors found to delete'
1423 );
1424 }
1425
1426 // Delete vectors in batches (Pinecone has limits on batch operations)
1427 $batch_size = 100;
1428 $batches = array_chunk($all_vector_ids, $batch_size);
1429 $deleted_count = 0;
1430 $failed_batches = 0;
1431
1432 foreach ($batches as $batch) {
1433 $result = $this->mxchat_delete_pinecone_batch($batch, $api_key, $host);
1434 if ($result['success']) {
1435 $deleted_count += count($batch);
1436 } else {
1437 $failed_batches++;
1438 }
1439 }
1440
1441 if ($failed_batches > 0) {
1442 return array(
1443 'success' => false,
1444 'message' => sprintf('Deleted %d vectors, but %d batches failed', $deleted_count, $failed_batches)
1445 );
1446 }
1447
1448 return array(
1449 'success' => true,
1450 'message' => "Successfully deleted {$deleted_count} vectors from Pinecone"
1451 );
1452
1453 } catch (Exception $e) {
1454 return array(
1455 'success' => false,
1456 'message' => $e->getMessage()
1457 );
1458 }
1459 }
1460
1461 /**
1462 * Deletes batch of vectors from Pinecone database
1463 */
1464 private function mxchat_delete_pinecone_batch($vector_ids, $api_key, $host) {
1465 // Build the API endpoint
1466 $api_endpoint = "https://{$host}/vectors/delete";
1467
1468 // Prepare the request body with the IDs
1469 $request_body = array(
1470 'ids' => $vector_ids
1471 );
1472
1473 // Make the deletion request
1474 $response = wp_remote_post($api_endpoint, array(
1475 'headers' => array(
1476 'Api-Key' => $api_key,
1477 'accept' => 'application/json',
1478 'content-type' => 'application/json'
1479 ),
1480 'body' => wp_json_encode($request_body),
1481 'timeout' => 60, // Increased timeout for batch operations
1482 'method' => 'POST'
1483 ));
1484
1485 // Handle WordPress HTTP API errors
1486 if (is_wp_error($response)) {
1487 return array(
1488 'success' => false,
1489 'message' => $response->get_error_message()
1490 );
1491 }
1492
1493 // Check response status
1494 $response_code = wp_remote_retrieve_response_code($response);
1495 $response_body = wp_remote_retrieve_body($response);
1496
1497 // Pinecone returns 200 for successful deletion
1498 if ($response_code !== 200) {
1499 //error_log('Pinecone batch deletion failed: HTTP ' . $response_code . ' - ' . $response_body);
1500 return array(
1501 'success' => false,
1502 'message' => sprintf(
1503 'Pinecone API error (HTTP %d): %s',
1504 $response_code,
1505 $response_body
1506 )
1507 );
1508 }
1509
1510 return array(
1511 'success' => true,
1512 'message' => 'Batch deleted successfully from Pinecone'
1513 );
1514 }
1515
1516
1517 /**
1518 * Deletes vector from Pinecone using API request
1519 */
1520 public function mxchat_delete_from_pinecone_by_vector_id($vector_id, $api_key, $host, $namespace = '') {
1521 //error_log('=== PINECONE DELETE OPERATION ===');
1522 //error_log('Vector ID: ' . $vector_id);
1523 //error_log('Host: ' . $host);
1524 //error_log('API Key: ' . (empty($api_key) ? 'EMPTY' : 'SET'));
1525
1526 // First, let's verify the vector exists before trying to delete
1527 $fetch_url = "https://{$host}/vectors/fetch";
1528
1529 $fetch_params = array(
1530 'ids' => array($vector_id)
1531 );
1532
1533 // Add namespace if provided (though you said you're not using namespaces)
1534 if (!empty($namespace)) {
1535 $fetch_params['namespace'] = $namespace;
1536 }
1537
1538 // Construct URL with query parameters for GET request
1539 $fetch_url_with_params = $fetch_url . '?' . http_build_query($fetch_params);
1540
1541 $fetch_response = wp_remote_get($fetch_url_with_params, array(
1542 'headers' => array(
1543 'Api-Key' => $api_key,
1544 'accept' => 'application/json'
1545 ),
1546 'timeout' => 15
1547 ));
1548
1549 if (!is_wp_error($fetch_response) && wp_remote_retrieve_response_code($fetch_response) === 200) {
1550 $fetch_body = wp_remote_retrieve_body($fetch_response);
1551 $fetch_data = json_decode($fetch_body, true);
1552
1553 //error_log('DEBUG: Fetch response: ' . print_r($fetch_data, true));
1554
1555 if (isset($fetch_data['vectors']) && isset($fetch_data['vectors'][$vector_id])) {
1556 //error_log('DEBUG: Vector EXISTS in this index before deletion');
1557 } else {
1558 //error_log('WARNING: Vector NOT FOUND in this index! It may be in a different bot\'s index');
1559 // You might want to return an error here
1560 }
1561 } else {
1562 //error_log('DEBUG: Could not fetch vector to verify existence');
1563 }
1564
1565 // Now proceed with deletion
1566 $api_endpoint = "https://{$host}/vectors/delete";
1567
1568 // Prepare the request body with the ID
1569 $request_body = array(
1570 'ids' => array($vector_id)
1571 );
1572
1573 // Add namespace if provided
1574 if (!empty($namespace)) {
1575 $request_body['namespace'] = $namespace;
1576 }
1577
1578 //error_log('DEBUG: Delete request body: ' . json_encode($request_body));
1579 //error_log('DEBUG: Delete endpoint: ' . $api_endpoint);
1580
1581 // Make the deletion request
1582 $response = wp_remote_post($api_endpoint, array(
1583 'headers' => array(
1584 'Api-Key' => $api_key,
1585 'accept' => 'application/json',
1586 'content-type' => 'application/json'
1587 ),
1588 'body' => wp_json_encode($request_body),
1589 'timeout' => 30
1590 ));
1591
1592 // Handle WordPress HTTP API errors
1593 if (is_wp_error($response)) {
1594 //error_log('DEBUG: WP Error: ' . $response->get_error_message());
1595 return array(
1596 'success' => false,
1597 'message' => $response->get_error_message()
1598 );
1599 }
1600
1601 // Check response status
1602 $response_code = wp_remote_retrieve_response_code($response);
1603 $response_body = wp_remote_retrieve_body($response);
1604
1605 //error_log('DEBUG: Delete response code: ' . $response_code);
1606 //error_log('DEBUG: Delete response body: ' . $response_body);
1607
1608 // Pinecone returns 200 for successful deletion (even if vector didn't exist)
1609 if ($response_code !== 200) {
1610 //error_log('DEBUG: Non-200 response from Pinecone');
1611 return array(
1612 'success' => false,
1613 'message' => sprintf(
1614 'Pinecone API error (HTTP %d): %s',
1615 $response_code,
1616 $response_body
1617 )
1618 );
1619 }
1620
1621 // After deletion, verify it's actually gone
1622 sleep(1); // Give Pinecone a moment to process
1623
1624 $verify_response = wp_remote_get($fetch_url_with_params, array(
1625 'headers' => array(
1626 'Api-Key' => $api_key,
1627 'accept' => 'application/json'
1628 ),
1629 'timeout' => 15
1630 ));
1631
1632 if (!is_wp_error($verify_response) && wp_remote_retrieve_response_code($verify_response) === 200) {
1633 $verify_body = wp_remote_retrieve_body($verify_response);
1634 $verify_data = json_decode($verify_body, true);
1635
1636 if (isset($verify_data['vectors']) && isset($verify_data['vectors'][$vector_id])) {
1637 //error_log('ERROR: Vector STILL EXISTS after deletion attempt!');
1638 return array(
1639 'success' => false,
1640 'message' => 'Vector still exists after deletion attempt'
1641 );
1642 } else {
1643 //error_log('SUCCESS: Vector confirmed deleted (or never existed)');
1644 }
1645 }
1646
1647 //error_log('=== END PINECONE DELETE OPERATION ===');
1648
1649 return array(
1650 'success' => true,
1651 'message' => 'Vector deleted successfully from Pinecone'
1652 );
1653 }
1654
1655 /**
1656 * Deletes data from Pinecone using a source URL
1657 */
1658 public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
1659 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1660 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1661
1662 if (empty($host) || empty($api_key)) {
1663 //error_log('MXChat: Pinecone deletion failed - missing configuration');
1664 return false;
1665 }
1666
1667 $api_endpoint = "https://{$host}/vectors/delete";
1668 $vector_id = md5($source_url);
1669
1670 $request_body = array(
1671 'ids' => array($vector_id)
1672 );
1673
1674 $response = wp_remote_post($api_endpoint, array(
1675 'headers' => array(
1676 'Api-Key' => $api_key,
1677 'accept' => 'application/json',
1678 'content-type' => 'application/json'
1679 ),
1680 'body' => wp_json_encode($request_body),
1681 'timeout' => 30
1682 ));
1683
1684 if (is_wp_error($response)) {
1685 //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
1686 return false;
1687 }
1688
1689 $response_code = wp_remote_retrieve_response_code($response);
1690 if ($response_code !== 200) {
1691 //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
1692 return false;
1693 }
1694
1695 return true;
1696 }
1697
1698
1699 /**
1700 * Deletes data from Pinecone index using API key
1701 */
1702 private function mxchat_delete_from_pinecone($urls, $api_key, $environment, $index_name) {
1703 // Get the Pinecone host from options (matching your store_in_pinecone_main pattern)
1704 $options = get_option('mxchat_pinecone_addon_options');
1705 $host = $options['mxchat_pinecone_host'] ?? '';
1706
1707 if (empty($host)) {
1708 return array(
1709 'success' => false,
1710 'message' => 'Pinecone host is not configured. Please set the host in your settings.'
1711 );
1712 }
1713
1714 // Build API endpoint using the configured host
1715 $api_endpoint = "https://{$host}/vectors/delete";
1716
1717 // Create vector IDs from URLs (matching your store method's ID generation)
1718 $vector_ids = array_map('md5', $urls);
1719
1720 // Prepare the delete request body
1721 $request_body = array(
1722 'ids' => $vector_ids,
1723 'filter' => array(
1724 'source_url' => array(
1725 '$in' => $urls
1726 )
1727 )
1728 );
1729
1730 // Make the deletion request
1731 $response = wp_remote_post($api_endpoint, array(
1732 'headers' => array(
1733 'Api-Key' => $api_key,
1734 'accept' => 'application/json',
1735 'content-type' => 'application/json'
1736 ),
1737 'body' => wp_json_encode($request_body),
1738 'timeout' => 30,
1739 'data_format' => 'body'
1740 ));
1741
1742 // Handle WordPress HTTP API errors
1743 if (is_wp_error($response)) {
1744 return array(
1745 'success' => false,
1746 'message' => $response->get_error_message()
1747 );
1748 }
1749
1750 // Check response status
1751 $response_code = wp_remote_retrieve_response_code($response);
1752 if ($response_code !== 200) {
1753 $body = wp_remote_retrieve_body($response);
1754 return array(
1755 'success' => false,
1756 'message' => sprintf(
1757 'Pinecone API error (HTTP %d): %s',
1758 $response_code,
1759 $body
1760 )
1761 );
1762 }
1763
1764 // Parse response body
1765 $body = wp_remote_retrieve_body($response);
1766 $response_data = json_decode($body, true);
1767
1768 // Final validation of the response
1769 if (json_last_error() !== JSON_ERROR_NONE) {
1770 return array(
1771 'success' => false,
1772 'message' => 'Failed to parse Pinecone response: ' . json_last_error_msg()
1773 );
1774 }
1775
1776 return array(
1777 'success' => true,
1778 'message' => sprintf('Successfully deleted %d vectors from Pinecone', count($vector_ids))
1779 );
1780 }
1781
1782
1783
1784 /**
1785 * Retrieves processed content from Pinecone API
1786 *
1787 * @param array $pinecone_options Pinecone configuration options
1788 * @param array $post_ids Optional array of specific post IDs to check (if empty, checks all)
1789 * @return array Processed data keyed by post ID
1790 */
1791 public function mxchat_get_pinecone_processed_content($pinecone_options, $post_ids = array()) {
1792 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1793 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1794
1795 if (empty($api_key) || empty($host)) {
1796 return array();
1797 }
1798
1799 $pinecone_data = array();
1800
1801 try {
1802 // Always get fresh data from Pinecone
1803 $pinecone_data = $this->mxchat_scan_pinecone_for_processed_content($pinecone_options, $post_ids);
1804
1805 // Method 2: Final fallback - try stats endpoint (if available)
1806 if (empty($pinecone_data)) {
1807 $stats_url = "https://{$host}/describe_index_stats";
1808
1809 $response = wp_remote_post($stats_url, array(
1810 'headers' => array(
1811 'Api-Key' => $api_key,
1812 'Content-Type' => 'application/json'
1813 ),
1814 'body' => json_encode(array()),
1815 'timeout' => 30
1816 ));
1817
1818 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
1819 $body = wp_remote_retrieve_body($response);
1820 $stats_data = json_decode($body, true);
1821 }
1822 }
1823
1824 } catch (Exception $e) {
1825 // Log error but don't return cached data
1826 }
1827
1828 return $pinecone_data;
1829 }
1830 // ========================================
1831 // HELPER METHODS
1832 // ========================================
1833
1834 /**
1835 * Validates Pinecone API credentials
1836 */
1837 private function mxchat_validate_pinecone_credentials($api_key, $host) {
1838 if (empty($api_key) || empty($host)) {
1839 return false;
1840 }
1841 return true;
1842 }
1843
1844 /**
1845 * Get Pinecone API credentials from options
1846 */
1847 private function mxchat_get_pinecone_credentials() {
1848 $options = get_option('mxchat_options', array());
1849 return array(
1850 'api_key' => isset($options['pinecone_api_key']) ? $options['pinecone_api_key'] : '',
1851 'host' => isset($options['pinecone_host']) ? $options['pinecone_host'] : ''
1852 );
1853 }
1854
1855 /**
1856 * Log Pinecone operation errors
1857 */
1858 private function log_pinecone_error($operation, $error_message) {
1859 //error_log("MxChat Pinecone {$operation} Error: " . $error_message);
1860 }
1861
1862 // ========================================
1863 // STATIC ACCESS METHODS (for backward compatibility)
1864 // ========================================
1865
1866 /**
1867 * Get singleton instance
1868 */
1869 public static function get_instance() {
1870 static $instance = null;
1871 if ($instance === null) {
1872 $instance = new self();
1873 }
1874 return $instance;
1875 }
1876 }
1877
1878 // Initialize the Pinecone manager
1879 $mxchat_pinecone_manager = MxChat_Pinecone_Manager::get_instance();
1880