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

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

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