PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.8
MxChat – AI Chatbot & Content Generation for WordPress v3.2.8
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | includes/class-mxchat-utils.php +115 -12 3.2.33.2.8 View file →
@@ -255,10 +255,8 @@
255 255 $max_attempts = 3;
256 256 $current_content = $safe_content;
257 257 $result = false;
258 258
259 - $active_model = self::get_active_embedding_model();
260 -
261 259 while ($attempt <= $max_attempts && $result === false) {
262 260 try {
263 261 if ($existing_id) {
264 262 //error_log('[MXCHAT-DB] Found existing entry (ID: ' . $existing_id . '). Updating... (Attempt ' . $attempt . ')');
@@ -271,13 +269,12 @@
271 269 'article_content' => $current_content,
272 270 'embedding_vector' => $embedding_vector_serialized,
273 271 'source_url' => $source_url,
274 272 'content_type' => $content_type,
275 - 'embedding_model' => $active_model,
276 273 'timestamp' => current_time('mysql'),
277 274 ),
278 275 array('id' => $existing_id),
279 - array('%s','%s','%s','%s','%s','%s','%s'),
276 + array('%s','%s','%s','%s','%s','%s'),
280 277 array('%d')
281 278 );
282 279 } else {
283 280 //error_log('[MXCHAT-DB] No existing entry found. Inserting new row... (Attempt ' . $attempt . ')');
@@ -291,12 +288,11 @@
291 288 'article_content' => $current_content,
292 289 'embedding_vector' => $embedding_vector_serialized,
293 290 'source_url' => $source_url, // Now unique for manual content
294 291 'content_type' => $content_type,
295 - 'embedding_model' => $active_model,
296 292 'timestamp' => current_time('mysql'),
297 293 ),
298 - array('%s','%s','%s','%s','%s','%s','%s')
294 + array('%s','%s','%s','%s','%s','%s')
299 295 );
300 296 }
301 297
302 298 if ($result === false) {
@@ -419,9 +415,8 @@
419 415 'type' => $content_type, // Now supports: post, page, pdf, url, manual, product, etc.
420 416 'last_updated' => time(),
421 417 'created_at' => time(), // Add creation timestamp
422 418 'bot_id' => $bot_id, // Add bot identification
423 - 'embedding_model' => self::get_active_embedding_model() // 3.2.3: track which model produced this vector
424 419 );
425 420
426 421 $vector_data = array(
427 422 'id' => $vector_id,
@@ -493,11 +488,20 @@
493 488 } else {
494 489 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
495 490 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
496 491 }
497 -
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 +
498 502 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
499 -
503 +
500 504 // Determine endpoint and API key based on model
501 505 if (strpos($selected_model, 'voyage') === 0) {
502 506 $endpoint = 'https://api.voyageai.com/v1/embeddings';
503 507 $api_key = $options['voyage_api_key'] ?? '';
@@ -588,8 +592,81 @@
588 592 }
589 593 }
590 594
591 595 /**
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 +/**
592 669 * Submit content as multiple chunks
593 670 *
594 671 * Splits large content into chunks, generates embeddings for each,
595 672 * and stores them with chunk metadata for later reassembly.
@@ -636,8 +713,36 @@
636 713
637 714 foreach ($chunks as $index => $chunk_text) {
638 715 // Generate chunk metadata
639 716 $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 +
640 745 $chunk_vector_id = MxChat_Chunker::generate_chunk_vector_id($source_url, $index);
641 746
642 747 //error_log('[MXCHAT-CHUNK] Processing chunk ' . ($index + 1) . '/' . $total_chunks . ' (ID: ' . $chunk_vector_id . ')');
643 748
@@ -737,9 +842,8 @@
737 842 'parent_url_hash' => $chunk_metadata['parent_url_hash'],
738 843 'last_updated' => time(),
739 844 'created_at' => time(),
740 845 'bot_id' => $bot_id,
741 - 'embedding_model' => self::get_active_embedding_model()
742 846 );
743 847
744 848 $vector_data = array(
745 849 'id' => $vector_id,
@@ -792,12 +896,11 @@
792 896 'article_content' => $content_with_metadata,
793 897 'embedding_vector' => $embedding_vector_serialized,
794 898 'source_url' => $source_url,
795 899 'content_type' => $content_type,
796 - 'embedding_model' => self::get_active_embedding_model(),
797 900 'timestamp' => current_time('mysql')
798 901 ),
799 - array('%s', '%s', '%s', '%s', '%s', '%s', '%s')
902 + array('%s', '%s', '%s', '%s', '%s', '%s')
800 903 );
801 904
802 905 if ($result === false) {
803 906 return new WP_Error('database_failed', 'Failed to insert chunk: ' . $wpdb->last_error);