PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.5
MxChat – AI Chatbot & Content Generation for WordPress v3.1.5
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
← All changes | includes/class-mxchat-utils.php +23 -191 3.2.113.1.5 View file →
@@ -5,51 +5,8 @@
5 5
6 6 class MxChat_Utils {
7 7
8 8 /**
9 - * Centralized embedding model registry. Single source of truth for dimensions
10 - * and provider, so model-switch protection logic doesn't drift across files.
11 - */
12 -public static function embedding_model_registry() {
13 - return array(
14 - 'text-embedding-ada-002' => array('dims' => 1536, 'provider' => 'openai', 'label' => 'Ada 2'),
15 - 'text-embedding-3-small' => array('dims' => 1536, 'provider' => 'openai', 'label' => 'TE3 Small'),
16 - 'text-embedding-3-large' => array('dims' => 3072, 'provider' => 'openai', 'label' => 'TE3 Large'),
17 - 'voyage-3-large' => array('dims' => 2048, 'provider' => 'voyage', 'label' => 'Voyage-3 Large'),
18 - 'gemini-embedding-001' => array('dims' => 1536, 'provider' => 'gemini', 'label' => 'Gemini Embedding'),
19 - );
20 -}
21 -
22 -public static function embedding_model_dimensions($model) {
23 - $registry = self::embedding_model_registry();
24 - return isset($registry[$model]) ? (int) $registry[$model]['dims'] : 0;
25 -}
26 -
27 -public static function embedding_model_label($model) {
28 - $registry = self::embedding_model_registry();
29 - return isset($registry[$model]) ? $registry[$model]['label'] : $model;
30 -}
31 -
32 -/**
33 - * Returns the model that was last used to actually write embeddings into the
34 - * KB. Differs from the user-selected setting once a switch has happened but
35 - * no re-embed has occurred yet — that's the mismatch state we warn about.
36 - */
37 -public static function get_active_embedding_model() {
38 - return get_option('mxchat_active_embedding_model', '');
39 -}
40 -
41 -/**
42 - * Stamp the model that produced the most recent successful embedding. Called
43 - * from generate_embedding() right after the API responds with a valid vector.
44 - */
45 -public static function stamp_active_embedding_model($model) {
46 - if (!empty($model) && $model !== self::get_active_embedding_model()) {
47 - update_option('mxchat_active_embedding_model', $model, false);
48 - }
49 -}
50 -
51 -/**
52 9 * UPDATED: Submit or update content (and its embedding) in the database.
53 10 * Stores in Pinecone if enabled, otherwise stores in WordPress DB.
54 11 *
55 12 * @param string $content The content to be embedded.
@@ -224,11 +181,9 @@
224 181 // Check if this is truly manual content (no URL at all) vs a real URL that filter_var rejects
225 182 // filter_var(FILTER_VALIDATE_URL) rejects valid URLs with encoded chars, non-ASCII, fragments, etc.
226 183 // Use a looser check: if it starts with http(s):// or has a scheme, it's a URL
227 184 $has_url_scheme = !empty($source_url) && preg_match('#^https?://#i', $source_url);
228 - // Treat legacy mxchat.ai source URLs as manual — old bug assigned the site URL to manual entries
229 - $is_legacy_mxchat_url = $has_url_scheme && strpos($source_url, 'mxchat.ai') !== false;
230 - $is_manual_content = empty($source_url) || $source_url === '' || !$has_url_scheme || $is_legacy_mxchat_url;
185 + $is_manual_content = empty($source_url) || $source_url === '' || !$has_url_scheme;
231 186
232 187 if ($is_manual_content) {
233 188 // Generate unique identifier for manual content to prevent overwrites
234 189 $source_url = 'mxchat://manual-content/' . time() . '-' . wp_generate_password(8, false);
@@ -414,9 +369,9 @@
414 369 'source_url' => $url, // Can be empty for manual content
415 370 'type' => $content_type, // Now supports: post, page, pdf, url, manual, product, etc.
416 371 'last_updated' => time(),
417 372 'created_at' => time(), // Add creation timestamp
418 - 'bot_id' => $bot_id, // Add bot identification
373 + 'bot_id' => $bot_id // Add bot identification
419 374 );
420 375
421 376 $vector_data = array(
422 377 'id' => $vector_id,
@@ -488,20 +443,11 @@
488 443 } else {
489 444 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
490 445 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
491 446 }
492 -
493 - // Opt-in: when the custom provider is selected for embeddings, route the KB
494 - // INDEX side through the same custom endpoint the query side uses, so stored
495 - // vectors and query vectors come from the same model. Default-off behavior
496 - // below is untouched.
497 - if (isset($options['custom_provider_for_embeddings']) && $options['custom_provider_for_embeddings'] === 'on') {
498 - $custom = self::generate_embedding_custom($text, $options);
499 - return is_array($custom) ? $custom : null;
500 - }
501 -
447 +
502 448 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
503 -
449 +
504 450 // Determine endpoint and API key based on model
505 451 if (strpos($selected_model, 'voyage') === 0) {
506 452 $endpoint = 'https://api.voyageai.com/v1/embeddings';
507 453 $api_key = $options['voyage_api_key'] ?? '';
@@ -573,9 +519,8 @@
573 519 // Handle different response formats based on provider
574 520 if (strpos($selected_model, 'gemini-embedding') === 0) {
575 521 // Gemini API response format
576 522 if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
577 - self::stamp_active_embedding_model($selected_model);
578 523 return $response_body['embedding']['values'];
579 524 } else {
580 525 //error_log('Invalid response received from Gemini embedding API for bot ' . $bot_id . ': ' . wp_json_encode($response_body));
581 526 return null;
@@ -582,9 +527,8 @@
582 527 }
583 528 } else {
584 529 // OpenAI/Voyage API response format
585 530 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
586 - self::stamp_active_embedding_model($selected_model);
587 531 return $response_body['data'][0]['embedding'];
588 532 } else {
589 533 //error_log('Invalid response received from embedding API for bot ' . $bot_id . ': ' . wp_json_encode($response_body));
590 534 return null;
@@ -592,81 +536,8 @@
592 536 }
593 537 }
594 538
595 539 /**
596 - * Generate an embedding via a Custom (OpenAI-compatible) provider's /embeddings route.
597 - * Shared by every embedding entry point so the KNOWLEDGE-BASE INDEX side and the
598 - * QUERY side route through the same model when the opt-in
599 - * 'custom_provider_for_embeddings' setting is on. Mirrors the query-path logic in
600 - * MxChat_Integrator::mxchat_generate_embedding_custom() but takes an explicit
601 - * $options array so it is callable statically from utils + knowledge-manager.
602 - *
603 - * Returns a numeric array (the embedding vector) on success, or a human-readable
604 - * error string on failure (so callers expecting a string error, like the
605 - * knowledge-manager, can surface it directly; callers expecting array|null wrap it).
606 - *
607 - * @param string $text Text to embed.
608 - * @param array $options The resolved mxchat options (must contain the custom_provider_* keys).
609 - * @return array|string Embedding vector on success; error string on failure.
610 - */
611 -public static function generate_embedding_custom($text, $options) {
612 - if (empty($text)) {
613 - return 'No text provided for embedding generation';
614 - }
615 -
616 - $base_url = isset($options['custom_provider_base_url']) ? rtrim(trim((string) $options['custom_provider_base_url']), '/') : '';
617 - if (empty($base_url)) {
618 - return 'Custom provider Base URL is not configured.';
619 - }
620 -
621 - $api_key = isset($options['custom_provider_api_key']) ? trim((string) $options['custom_provider_api_key']) : '';
622 - $auth_scheme = isset($options['custom_provider_auth_scheme']) ? $options['custom_provider_auth_scheme'] : 'bearer';
623 - $api_version = isset($options['custom_provider_api_version']) ? trim((string) $options['custom_provider_api_version']) : '';
624 -
625 - // Embedding model: prefer the dedicated custom_provider_embedding_model, fall back to the chat model.
626 - $model = (isset($options['custom_provider_embedding_model']) && trim((string) $options['custom_provider_embedding_model']) !== '')
627 - ? trim((string) $options['custom_provider_embedding_model'])
628 - : ((isset($options['custom_provider_model']) && trim((string) $options['custom_provider_model']) !== '') ? trim((string) $options['custom_provider_model']) : 'default');
629 -
630 - $embed_url = $base_url . '/embeddings';
631 - if (!empty($api_version)) {
632 - $embed_url .= (strpos($embed_url, '?') === false ? '?' : '&') . 'api-version=' . rawurlencode($api_version);
633 - }
634 -
635 - $headers = ['Content-Type' => 'application/json'];
636 - if (!empty($api_key)) {
637 - if ($auth_scheme === 'api-key') {
638 - $headers['api-key'] = $api_key;
639 - } else {
640 - $headers['Authorization'] = 'Bearer ' . $api_key;
641 - }
642 - }
643 -
644 - $response = wp_remote_post($embed_url, [
645 - 'headers' => $headers,
646 - 'body' => wp_json_encode(['input' => $text, 'model' => $model]),
647 - 'timeout' => 60,
648 - ]);
649 - if (is_wp_error($response)) {
650 - return 'Connection error when generating embeddings (custom provider): ' . $response->get_error_message();
651 - }
652 -
653 - $status = wp_remote_retrieve_response_code($response);
654 - $body = json_decode(wp_remote_retrieve_body($response), true);
655 - if ($status !== 200) {
656 - $msg = isset($body['error']['message']) ? $body['error']['message'] : 'HTTP ' . $status;
657 - return 'Custom embedding endpoint error: ' . $msg;
658 - }
659 - if (isset($body['data'][0]['embedding']) && is_array($body['data'][0]['embedding'])) {
660 - // Stamp the custom model identity so the active-embedding-model mismatch
661 - // warning reflects the real (custom) model rather than the built-in setting.
662 - self::stamp_active_embedding_model('custom:' . $model);
663 - return $body['data'][0]['embedding'];
664 - }
665 - return 'Invalid embedding response from custom provider.';
666 -}
667 -
668 -/**
669 540 * Submit content as multiple chunks
670 541 *
671 542 * Splits large content into chunks, generates embeddings for each,
672 543 * and stores them with chunk metadata for later reassembly.
@@ -713,36 +584,8 @@
713 584
714 585 foreach ($chunks as $index => $chunk_text) {
715 586 // Generate chunk metadata
716 587 $chunk_metadata = MxChat_Chunker::create_chunk_metadata($index, $total_chunks, $source_url);
717 -
718 - // AI-Engine-style aliases so external consumers (Pinecone/Qdrant/Chroma) can rely on
719 - // a stable shorthand ('source'/'part_index'/'part_total') without parsing our internal names.
720 - $chunk_metadata['source'] = $source_url;
721 - $chunk_metadata['part_index'] = (int) $index;
722 - $chunk_metadata['part_total'] = (int) $total_chunks;
723 -
724 - /**
725 - * Filter the per-chunk metadata blob before it's written to the KB store.
726 - *
727 - * @param array $chunk_metadata Metadata array (source, part_index, part_total, chunk_index, total_chunks, source_url, parent_url_hash, document_type, ...).
728 - * @param string $chunk_text The chunk text being stored.
729 - * @param array $context ['bot_id' => string, 'content_type' => string, 'source_url' => string, 'part_index' => int, 'part_total' => int]
730 - * @return array Updated metadata array.
731 - */
732 - $chunk_metadata = apply_filters(
733 - 'mxchat_embedding_chunk_metadata',
734 - $chunk_metadata,
735 - $chunk_text,
736 - array(
737 - 'bot_id' => $bot_id,
738 - 'content_type' => $content_type,
739 - 'source_url' => $source_url,
740 - 'part_index' => (int) $index,
741 - 'part_total' => (int) $total_chunks,
742 - )
743 - );
744 -
745 588 $chunk_vector_id = MxChat_Chunker::generate_chunk_vector_id($source_url, $index);
746 589
747 590 //error_log('[MXCHAT-CHUNK] Processing chunk ' . ($index + 1) . '/' . $total_chunks . ' (ID: ' . $chunk_vector_id . ')');
748 591
@@ -841,9 +684,9 @@
841 684 'total_chunks' => $chunk_metadata['total_chunks'],
842 685 'parent_url_hash' => $chunk_metadata['parent_url_hash'],
843 686 'last_updated' => time(),
844 687 'created_at' => time(),
845 - 'bot_id' => $bot_id,
688 + 'bot_id' => $bot_id
846 689 );
847 690
848 691 $vector_data = array(
849 692 'id' => $vector_id,
@@ -959,34 +802,31 @@
959 802
960 803 // Add the original single-vector ID (for non-chunked content)
961 804 $vectors_to_delete[] = $base_vector_id;
962 805
963 - // Pinecone /vectors/list is a GET endpoint with query-string parameters; a POST here returns a
964 - // non-200 silently and we end up only deleting the base vector, leaving chunks orphaned.
965 - $query_params = array(
806 + // Use Pinecone list API to find all chunk vectors with this prefix
807 + $list_url = "https://{$host}/vectors/list";
808 +
809 + $list_body = array(
966 810 'prefix' => $base_vector_id . '_chunk_',
967 - 'limit' => 100,
811 + 'limit' => 100
968 812 );
813 +
969 814 if (!empty($namespace)) {
970 - $query_params['namespace'] = $namespace;
815 + $list_body['namespace'] = $namespace;
971 816 }
972 817
973 - $list_url = "https://{$host}/vectors/list?" . http_build_query($query_params);
818 + $list_response = wp_remote_post($list_url, array(
819 + 'headers' => array(
820 + 'Api-Key' => $api_key,
821 + 'accept' => 'application/json',
822 + 'content-type' => 'application/json'
823 + ),
824 + 'body' => wp_json_encode($list_body),
825 + 'timeout' => 30
826 + ));
974 827
975 - // Paginate in case a URL has more than 100 chunks.
976 - do {
977 - $list_response = wp_remote_get($list_url, array(
978 - 'headers' => array(
979 - 'Api-Key' => $api_key,
980 - 'accept' => 'application/json',
981 - ),
982 - 'timeout' => 30,
983 - ));
984 -
985 - if (is_wp_error($list_response) || wp_remote_retrieve_response_code($list_response) !== 200) {
986 - break;
987 - }
988 -
828 + if (!is_wp_error($list_response)) {
989 829 $list_data = json_decode(wp_remote_retrieve_body($list_response), true);
990 830 if (!empty($list_data['vectors'])) {
991 831 foreach ($list_data['vectors'] as $vector) {
992 832 if (isset($vector['id'])) {
@@ -993,17 +833,9 @@
993 833 $vectors_to_delete[] = $vector['id'];
994 834 }
995 835 }
996 836 }
997 -
998 - $next_token = $list_data['pagination']['next'] ?? '';
999 - if (empty($next_token)) {
1000 - break;
1001 - }
1002 -
1003 - $query_params['paginationToken'] = $next_token;
1004 - $list_url = "https://{$host}/vectors/list?" . http_build_query($query_params);
1005 - } while (true);
837 + }
1006 838
1007 839 if (empty($vectors_to_delete)) {
1008 840 //error_log('[MXCHAT-CHUNK-DELETE] No vectors found to delete');
1009 841 return true;