PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.6.1
MxChat – AI Chatbot & Content Generation for WordPress v2.6.1
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 / includes / class-mxchat-utils.php

class-mxchat-utils.php in MxChat – AI Chatbot & Content Generation for WordPress 2.6.1, at includes/class-mxchat-utils.php

527 lines 22.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit; // Exit if accessed directly
4 }
5
6 class MxChat_Utils {
7
8 /**
9 * UPDATED: Submit or update content (and its embedding) in the database.
10 * Stores in Pinecone if enabled, otherwise stores in WordPress DB.
11 *
12 * @param string $content The content to be embedded.
13 * @param string $source_url The source URL of the content.
14 * @param string $api_key The API key used for generating embeddings.
15 * @param string $vector_id Optional vector ID for Pinecone (if not provided, will use md5 of URL)
16 * @param string $bot_id The bot ID for multi-bot support
17 * @param string $content_type The type of content (post, page, pdf, url, manual, product, etc.)
18 * @return bool|WP_Error True on success, WP_Error on failure
19 */
20 public static function submit_content_to_db($content, $source_url, $api_key, $vector_id = null, $bot_id = 'default', $content_type = 'content') {
21 global $wpdb;
22 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
23
24 //error_log('[MXCHAT-DB] Starting database submission for URL: ' . $source_url . ' (Bot: ' . $bot_id . ', Type: ' . $content_type . ')');
25 //error_log('[MXCHAT-DB] Content length: ' . strlen($content) . ' bytes');
26
27 // Sanitize the source URL
28 $source_url = esc_url_raw($source_url);
29
30 // Sanitize content_type
31 $content_type = sanitize_key($content_type);
32 if (empty($content_type)) {
33 $content_type = 'content'; // Fallback for backwards compatibility
34 }
35
36 // Just ensure UTF-8 validity without aggressive escaping
37 $safe_content = wp_check_invalid_utf8($content);
38 // Remove only null bytes and other control characters, but preserve newlines (\n = \x0A) and carriage returns (\r = \x0D)
39 $safe_content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $safe_content);
40
41 // UPDATED: Generate the embedding using bot-specific configuration
42 $embedding_vector = self::generate_embedding($content, $api_key, $bot_id);
43
44 if (!is_array($embedding_vector)) {
45 //error_log('[MXCHAT-DB] Error: Embedding generation failed');
46 return new WP_Error('embedding_failed', 'Failed to generate embedding for content');
47 }
48
49 //error_log('[MXCHAT-DB] Embedding generated successfully');
50
51 // UPDATED: Check if Pinecone is enabled for this specific bot
52 if (self::is_pinecone_enabled_for_bot($bot_id)) {
53 //error_log('[MXCHAT-DB] Pinecone is enabled for bot ' . $bot_id . ' - using Pinecone storage');
54 // Store in Pinecone only
55 return self::store_in_pinecone_only($embedding_vector, $content, $source_url, $vector_id, $bot_id, $content_type);
56 } else {
57 //error_log('[MXCHAT-DB] Pinecone not enabled for bot ' . $bot_id . ' - using WordPress storage');
58 // Store in WordPress database only
59 $embedding_vector_serialized = maybe_serialize($embedding_vector);
60 return self::store_in_wordpress_db($safe_content, $source_url, $embedding_vector_serialized, $table_name, $content_type);
61 }
62 }
63
64 /**
65 * UPDATED: Check if Pinecone is enabled and properly configured for a specific bot
66 */
67 private static function is_pinecone_enabled_for_bot($bot_id = 'default') {
68 // For default bot or when multi-bot is not active, use original method
69 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
70 return self::is_pinecone_enabled();
71 }
72
73 // Get bot-specific Pinecone configuration
74 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
75
76 if (empty($bot_pinecone_config)) {
77 // Fallback to default configuration
78 return self::is_pinecone_enabled();
79 }
80
81 $enabled_check = !empty($bot_pinecone_config['use_pinecone']) && $bot_pinecone_config['use_pinecone'];
82 $api_key_check = !empty($bot_pinecone_config['api_key']);
83 $host_check = !empty($bot_pinecone_config['host']);
84
85 return $enabled_check && $api_key_check && $host_check;
86 }
87
88 /**
89 * Check if Pinecone is enabled and properly configured (original method for default bot)
90 */
91 private static function is_pinecone_enabled() {
92 $pinecone_options = get_option('mxchat_pinecone_addon_options');
93
94 if (empty($pinecone_options)) {
95 return false;
96 }
97
98 $enabled_check = !empty($pinecone_options['mxchat_use_pinecone']) && $pinecone_options['mxchat_use_pinecone'] !== '0';
99 $api_key_check = !empty($pinecone_options['mxchat_pinecone_api_key']);
100 $host_check = !empty($pinecone_options['mxchat_pinecone_host']);
101
102 return $enabled_check && $api_key_check && $host_check;
103 }
104
105 /**
106 * UPDATED: Store content in Pinecone only with bot support
107 */
108 private static function store_in_pinecone_only($embedding_vector, $content, $source_url, $vector_id = null, $bot_id = 'default', $content_type = 'content') {
109 //error_log('[MXCHAT-PINECONE] ===== Using Pinecone-only storage for bot ' . $bot_id . ' =====');
110
111 // Get bot-specific Pinecone configuration
112 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
113 $pinecone_options = get_option('mxchat_pinecone_addon_options');
114 $api_key = $pinecone_options['mxchat_pinecone_api_key'];
115 $environment = $pinecone_options['mxchat_pinecone_environment'] ?? '';
116 $index_name = $pinecone_options['mxchat_pinecone_index'] ?? '';
117 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
118 } else {
119 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
120 if (empty($bot_pinecone_config)) {
121 // Fallback to default configuration
122 $pinecone_options = get_option('mxchat_pinecone_addon_options');
123 $api_key = $pinecone_options['mxchat_pinecone_api_key'];
124 $environment = $pinecone_options['mxchat_pinecone_environment'] ?? '';
125 $index_name = $pinecone_options['mxchat_pinecone_index'] ?? '';
126 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
127 } else {
128 $api_key = $bot_pinecone_config['api_key'];
129 $environment = ''; // Not used in new Pinecone API
130 $index_name = ''; // Not used in new Pinecone API
131 $namespace = $bot_pinecone_config['namespace'] ?? '';
132 }
133 }
134
135 $result = self::store_in_pinecone_main(
136 $embedding_vector,
137 $content,
138 $source_url,
139 $api_key,
140 $environment,
141 $index_name,
142 $vector_id,
143 $bot_id,
144 $namespace,
145 $content_type
146 );
147
148 if (is_wp_error($result)) {
149 //error_log('[MXCHAT-PINECONE] Pinecone storage failed for bot ' . $bot_id . ': ' . $result->get_error_message());
150 return $result;
151 }
152
153 //error_log('[MXCHAT-PINECONE] Pinecone storage completed successfully for bot ' . $bot_id);
154 return true;
155 }
156
157 /**
158 * Store content in WordPress database with progressive fallback
159 * UPDATED 2.5.6: Now includes content_type parameter
160 */
161 private static function store_in_wordpress_db($safe_content, $source_url, $embedding_vector_serialized, $table_name, $content_type = 'content') {
162 global $wpdb;
163
164 //error_log('[MXCHAT-DB] ===== Using WordPress-only storage =====');
165
166 // Sanitize content_type
167 $content_type = sanitize_key($content_type);
168 if (empty($content_type)) {
169 $content_type = 'content'; // Fallback for backwards compatibility
170 }
171
172 // ===== FIXED: Generate unique identifier for manual content =====
173 $original_source_url = $source_url;
174 $is_manual_content = empty($source_url) || $source_url === '' || !filter_var($source_url, FILTER_VALIDATE_URL);
175
176 if ($is_manual_content) {
177 // Generate unique identifier for manual content to prevent overwrites
178 $source_url = 'mxchat://manual-content/' . time() . '-' . wp_generate_password(8, false);
179 //error_log('[MXCHAT-DB] Generated unique ID for manual content: ' . $source_url);
180 }
181
182 // Only check for duplicates if we have a valid source URL (not manual content)
183 $existing_id = null;
184 if (!$is_manual_content) {
185 $existing_id = $wpdb->get_var(
186 $wpdb->prepare(
187 "SELECT id FROM {$table_name} WHERE source_url = %s LIMIT 1",
188 $source_url
189 )
190 );
191 //error_log('[MXCHAT-DB] Checked for existing URL, found ID: ' . ($existing_id ?: 'none'));
192 } else {
193 //error_log('[MXCHAT-DB] Manual content - will create new entry (no duplicate check)');
194 }
195 // ===== END FIX =====
196
197 // Progressive fallback mechanism for problematic content
198 $attempt = 1;
199 $max_attempts = 3;
200 $current_content = $safe_content;
201 $result = false;
202
203 while ($attempt <= $max_attempts && $result === false) {
204 try {
205 if ($existing_id) {
206 //error_log('[MXCHAT-DB] Found existing entry (ID: ' . $existing_id . '). Updating... (Attempt ' . $attempt . ')');
207
208 // Update the existing row - UPDATED 2.5.6: Added content_type
209 $result = $wpdb->update(
210 $table_name,
211 array(
212 'url' => $source_url,
213 'article_content' => $current_content,
214 'embedding_vector' => $embedding_vector_serialized,
215 'source_url' => $source_url,
216 'content_type' => $content_type,
217 'timestamp' => current_time('mysql'),
218 ),
219 array('id' => $existing_id),
220 array('%s','%s','%s','%s','%s','%s'),
221 array('%d')
222 );
223 } else {
224 //error_log('[MXCHAT-DB] No existing entry found. Inserting new row... (Attempt ' . $attempt . ')');
225 //error_log('[MXCHAT-DB] Content sample: ' . substr($current_content, 0, 1000));
226
227 // Insert a new row - UPDATED 2.5.6: Added content_type
228 $result = $wpdb->insert(
229 $table_name,
230 array(
231 'url' => $source_url, // Now unique for manual content
232 'article_content' => $current_content,
233 'embedding_vector' => $embedding_vector_serialized,
234 'source_url' => $source_url, // Now unique for manual content
235 'content_type' => $content_type,
236 'timestamp' => current_time('mysql'),
237 ),
238 array('%s','%s','%s','%s','%s','%s')
239 );
240 }
241
242 if ($result === false) {
243 error_log('[MXCHAT-DB] Database operation failed (Attempt ' . $attempt . ')');
244 error_log('[MXCHAT-DB] MySQL Error: ' . $wpdb->last_error);
245 error_log('[MXCHAT-DB] MySQL Error Number: ' . $wpdb->last_errno);
246 error_log('[MXCHAT-DB] Last Query: ' . substr($wpdb->last_query, 0, 500));
247 error_log('[MXCHAT-DB] Content length: ' . strlen($current_content) . ' bytes');
248 error_log('[MXCHAT-DB] Embedding vector length: ' . strlen($embedding_vector_serialized) . ' bytes');
249
250 // Progressively apply more aggressive sanitization on failure
251 if ($attempt === 1) {
252 // First fallback: Use a more aggressive character filter and shorten
253 $current_content = preg_replace('/[^\p{L}\p{N}\s.,;:!?()-]/u', '', $current_content);
254 $current_content = substr($current_content, 0, 50000);
255 } else if ($attempt === 2) {
256 // Second fallback: Keep only alphanumeric and basic punctuation, shorten further
257 $current_content = preg_replace('/[^a-zA-Z0-9\s.,;:!?()-]/u', '', $current_content);
258 $current_content = substr($current_content, 0, 30000);
259 }
260
261 $attempt++;
262 }
263 } catch (Exception $e) {
264 //error_log('[MXCHAT-DB] Exception during database operation: ' . $e->getMessage());
265 $attempt++;
266 }
267 }
268
269 if ($result === false) {
270 error_log('[MXCHAT-DB] All database operation attempts failed');
271 error_log('[MXCHAT-DB] Final MySQL Error: ' . $wpdb->last_error);
272
273 $detailed_error = sprintf(
274 'Failed to store content in WordPress database after %d attempts. MySQL Error: %s (Error #%d). Content size: %d bytes, Embedding size: %d bytes',
275 $max_attempts,
276 $wpdb->last_error,
277 $wpdb->last_errno,
278 strlen($current_content),
279 strlen($embedding_vector_serialized)
280 );
281
282 return new WP_Error('database_failed', $detailed_error);
283 }
284
285 //error_log('[MXCHAT-DB] WordPress database operation completed successfully (Attempt ' . ($attempt - 1) . ')');
286 return true;
287 }
288
289 /**
290 * UPDATED: Store content in Pinecone database with bot support
291 * UPDATED 2.5.6: Now accepts content_type parameter
292 */
293 private static function store_in_pinecone_main($embedding_vector, $content, $url, $api_key, $environment, $index_name, $vector_id = null, $bot_id = 'default', $namespace = '', $content_type = 'content') {
294 //error_log('[MXCHAT-PINECONE-MAIN] ===== Starting Pinecone storage for bot ' . $bot_id . ' =====');
295
296 // ===== UPDATED: Handle manual content with unique vector IDs =====
297 if ($vector_id) {
298 // Use provided vector ID
299 //error_log('[MXCHAT-PINECONE-MAIN] Using provided vector ID: ' . $vector_id);
300 } elseif (!empty($url) && filter_var($url, FILTER_VALIDATE_URL)) {
301 // For valid URLs, use URL-based ID (existing behavior)
302 $vector_id = md5($url);
303 //error_log('[MXCHAT-PINECONE-MAIN] Generated vector ID from URL: ' . $vector_id);
304 } else {
305 // For manual content (empty/invalid URL), generate unique ID
306 $vector_id = 'manual_' . time() . '_' . substr(md5($content . microtime(true)), 0, 8);
307 //error_log('[MXCHAT-PINECONE-MAIN] Generated unique vector ID for manual content: ' . $vector_id);
308 }
309 // ===== END UPDATE =====
310
311 // Get host from bot-specific config or fallback to default
312 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
313 $options = get_option('mxchat_pinecone_addon_options');
314 $host = $options['mxchat_pinecone_host'] ?? '';
315 } else {
316 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
317 if (!empty($bot_pinecone_config)) {
318 $host = $bot_pinecone_config['host'] ?? '';
319 } else {
320 $options = get_option('mxchat_pinecone_addon_options');
321 $host = $options['mxchat_pinecone_host'] ?? '';
322 }
323 }
324
325 //error_log('[MXCHAT-PINECONE-MAIN] Host: ' . $host);
326 //error_log('[MXCHAT-PINECONE-MAIN] API key length: ' . strlen($api_key));
327 //error_log('[MXCHAT-PINECONE-MAIN] Bot ID: ' . $bot_id);
328 //error_log('[MXCHAT-PINECONE-MAIN] Namespace: ' . $namespace);
329
330 if (empty($host)) {
331 //error_log('[MXCHAT-PINECONE-MAIN] ERROR: Host is empty');
332 return new WP_Error('pinecone_config', 'Pinecone host is not configured. Please set the host in your bot settings.');
333 }
334
335 // ===== UPDATED 2.5.6: Use passed content_type or determine from URL if not provided =====
336 // Sanitize content_type
337 $content_type = sanitize_key($content_type);
338 if (empty($content_type)) {
339 // Fallback to old detection logic for backwards compatibility
340 $is_product = false;
341 $content_type = 'manual'; // Default for manual content
342
343 if (!empty($url) && filter_var($url, FILTER_VALIDATE_URL)) {
344 $is_product = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false);
345 $content_type = $is_product ? 'product' : 'content';
346 }
347 }
348
349 //error_log('[MXCHAT-PINECONE-MAIN] Content type: ' . $content_type);
350 // ===== END UPDATE =====
351
352 $api_endpoint = "https://{$host}/vectors/upsert";
353 //error_log('[MXCHAT-PINECONE-MAIN] API endpoint: ' . $api_endpoint);
354
355 // UPDATED 2.5.6: Use provided content_type in metadata
356 $metadata = array(
357 'text' => $content,
358 'source_url' => $url, // Can be empty for manual content
359 'type' => $content_type, // Now supports: post, page, pdf, url, manual, product, etc.
360 'last_updated' => time(),
361 'created_at' => time(), // Add creation timestamp
362 'bot_id' => $bot_id // Add bot identification
363 );
364
365 $vector_data = array(
366 'id' => $vector_id,
367 'values' => $embedding_vector,
368 'metadata' => $metadata
369 );
370
371 $request_body = array(
372 'vectors' => array($vector_data)
373 );
374
375 // Add namespace if specified for multi-bot separation
376 if (!empty($namespace)) {
377 $request_body['namespace'] = $namespace;
378 //error_log('[MXCHAT-PINECONE-MAIN] Using namespace: ' . $namespace);
379 }
380
381 //error_log('[MXCHAT-PINECONE-MAIN] Request body prepared (embedding dimensions: ' . count($embedding_vector) . ')');
382
383 $response = wp_remote_post($api_endpoint, array(
384 'headers' => array(
385 'Api-Key' => $api_key,
386 'accept' => 'application/json',
387 'content-type' => 'application/json'
388 ),
389 'body' => wp_json_encode($request_body),
390 'timeout' => 30,
391 'data_format' => 'body'
392 ));
393
394 if (is_wp_error($response)) {
395 //error_log('[MXCHAT-PINECONE-MAIN] WordPress request error: ' . $response->get_error_message());
396 return new WP_Error('pinecone_request', $response->get_error_message());
397 }
398
399 $response_code = wp_remote_retrieve_response_code($response);
400 //error_log('[MXCHAT-PINECONE-MAIN] Response code: ' . $response_code);
401
402 if ($response_code !== 200) {
403 $body = wp_remote_retrieve_body($response);
404 //error_log('[MXCHAT-PINECONE-MAIN] API error - Response body: ' . $body);
405 return new WP_Error('pinecone_api', sprintf(
406 'Pinecone API error (HTTP %d): %s',
407 $response_code,
408 $body
409 ));
410 }
411
412 $response_body = wp_remote_retrieve_body($response);
413 //error_log('[MXCHAT-PINECONE-MAIN] Success response: ' . $response_body);
414 //error_log('[MXCHAT-PINECONE-MAIN] Successfully stored in Pinecone for bot ' . $bot_id);
415 //error_log('[MXCHAT-PINECONE-MAIN] ===== Pinecone storage complete =====');
416
417 return true;
418 }
419
420 /**
421 * UPDATED: Generate an embedding for the given text using bot-specific configuration.
422 *
423 * @param string $text The text to be embedded.
424 * @param string $api_key The API key used for generating embeddings.
425 * @param string $bot_id The bot ID for multi-bot support
426 * @return array|null The embedding vector or null on failure.
427 */
428 private static function generate_embedding($text, $api_key, $bot_id = 'default') {
429 // Get bot-specific options
430 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
431 $options = get_option('mxchat_options');
432 } else {
433 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
434 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
435 }
436
437 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
438
439 // Determine endpoint and API key based on model
440 if (strpos($selected_model, 'voyage') === 0) {
441 $endpoint = 'https://api.voyageai.com/v1/embeddings';
442 $api_key = $options['voyage_api_key'] ?? '';
443 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
444 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
445 $api_key = $options['gemini_api_key'] ?? '';
446 } else {
447 $endpoint = 'https://api.openai.com/v1/embeddings';
448 // Use the bot-specific API key or fallback to passed API key
449 $api_key = $options['api_key'] ?? $api_key;
450 }
451
452 // Prepare request body based on provider
453 if (strpos($selected_model, 'gemini-embedding') === 0) {
454 // Gemini API format
455 $request_body = [
456 'model' => 'models/' . $selected_model,
457 'content' => [
458 'parts' => [
459 ['text' => $text]
460 ]
461 ],
462 'outputDimensionality' => 1536
463 ];
464
465 // Prepare headers for Gemini (API key as query parameter)
466 $endpoint .= '?key=' . $api_key;
467 $headers = [
468 'Content-Type' => 'application/json'
469 ];
470 } else {
471 // OpenAI/Voyage API format
472 $request_body = [
473 'input' => $text,
474 'model' => $selected_model
475 ];
476
477 // Add output_dimension for voyage-3-large
478 if ($selected_model === 'voyage-3-large') {
479 $request_body['output_dimension'] = 2048;
480 }
481
482 // Prepare headers for OpenAI/Voyage
483 $headers = [
484 'Content-Type' => 'application/json',
485 'Authorization' => 'Bearer ' . $api_key
486 ];
487 }
488
489 $args = [
490 'body' => wp_json_encode($request_body),
491 'headers' => $headers,
492 'timeout' => 60,
493 'redirection' => 5,
494 'blocking' => true,
495 'httpversion' => '1.0',
496 'sslverify' => true,
497 ];
498
499 $response = wp_remote_post($endpoint, $args);
500
501 if (is_wp_error($response)) {
502 //error_log('Error generating embedding for bot ' . $bot_id . ': ' . $response->get_error_message());
503 return null;
504 }
505
506 $response_body = json_decode(wp_remote_retrieve_body($response), true);
507
508 // Handle different response formats based on provider
509 if (strpos($selected_model, 'gemini-embedding') === 0) {
510 // Gemini API response format
511 if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
512 return $response_body['embedding']['values'];
513 } else {
514 //error_log('Invalid response received from Gemini embedding API for bot ' . $bot_id . ': ' . wp_json_encode($response_body));
515 return null;
516 }
517 } else {
518 // OpenAI/Voyage API response format
519 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
520 return $response_body['data'][0]['embedding'];
521 } else {
522 //error_log('Invalid response received from embedding API for bot ' . $bot_id . ': ' . wp_json_encode($response_body));
523 return null;
524 }
525 }
526 }
527 }